  1597→    std::unordered_map<uint32_t, uint32_t> addr_remap;
  1598→    {
  1599→        std::vector<uint32_t> sorted_ids(used_addr_ids.begin(), used_addr_ids.end());
  1600→        std::sort(sorted_ids.begin(), sorted_ids.end());
  1601→        for (uint32_t old_id : sorted_ids) {
  1602→            uint32_t new_id = static_cast<uint32_t>(out.addr_points.size());
  1603→            addr_remap[old_id] = new_id;
  1604→            out.addr_points.push_back(full.addr_points[old_id]);
  1605→        }
  1606→    }
  1607→
  1608→    // --- Build remapped interp ways ---
  1609→    std::unordered_map<uint32_t, uint32_t> interp_remap;
  1610→    {
  1611→        std::vector<uint32_t> sorted_ids(used_interp_ids.begin(), used_interp_ids.end());
  1612→        std::sort(sorted_ids.begin(), sorted_ids.end());
  1613→        for (uint32_t old_id : sorted_ids) {
  1614→            uint32_t new_id = static_cast<uint32_t>(out.interp_ways.size());
  1615→            interp_remap[old_id] = new_id;
  1616→            const auto& iw = full.interp_ways[old_id];
  1617→            InterpWay niw = iw;
  1618→            niw.node_offset = static_cast<uint32_t>(out.interp_nodes.size());
  1619→            out.interp_ways.push_back(niw);
  1620→            for (uint8_t n = 0; n < iw.node_count; n++) {
  1621→                out.interp_nodes.push_back(full.interp_nodes[iw.node_offset + n]);
  1622→            }
  1623→        }
  1624→    }
  1625→
  1626→    // --- Build remapped admin polygons ---
  1627→    std::unordered_map<uint32_t, uint32_t> admin_remap;
  1628→    {
  1629→        std::vector<uint32_t> sorted_ids(used_admin_ids.begin(), used_admin_ids.end());
  1630→        std::sort(sorted_ids.begin(), sorted_ids.end());
  1631→        for (uint32_t old_id : sorted_ids) {
  1632→            uint32_t new_id = static_cast<uint32_t>(out.admin_polygons.size());
  1633→            admin_remap[old_id] = new_id;
  1634→            const auto& ap = full.admin_polygons[old_id];
  1635→            AdminPolygon nap = ap;
  1636→            nap.vertex_offset = static_cast<uint32_t>(out.admin_vertices.size());
  1637→            out.admin_polygons.push_back(nap);
  1638→            for (uint16_t v = 0; v < ap.vertex_count; v++) {
  1639→                out.admin_vertices.push_back(full.admin_vertices[ap.vertex_offset + v]);
  1640→            }
  1641→        }
  1642→    }
  1643→
  1644→    // --- Build remapped cell maps ---
  1645→    for (const auto& [cell_id, ids] : full.cell_to_ways) {
  1646→        if (!cell_in_bbox(cell_id, bbox)) continue;
  1647→        std::vector<uint32_t> new_ids;
  1648→        for (uint32_t id : ids) {
  1649→            auto it = way_remap.find(id);
  1650→            if (it != way_remap.end()) {
  1651→                new_ids.push_back(it->second);
  1652→            }
  1653→        }
  1654→        if (!new_ids.empty()) {
  1655→            out.cell_to_ways[cell_id] = std::move(new_ids);
  1656→        }
  1657→    }
  1658→
  1659→    for (const auto& [cell_id, ids] : full.cell_to_addrs) {
  1660→        if (!cell_in_bbox(cell_id, bbox)) continue;
  1661→        std::vector<uint32_t> new_ids;
  1662→        for (uint32_t id : ids) {
  1663→            auto it = addr_remap.find(id);
  1664→            if (it != addr_remap.end()) {
  1665→                new_ids.push_back(it->second);
  1666→            }
  1667→        }
  1668→        if (!new_ids.empty()) {
  1669→            out.cell_to_addrs[cell_id] = std::move(new_ids);
  1670→        }
  1671→    }
  1672→
  1673→    for (const auto& [cell_id, ids] : full.cell_to_interps) {
  1674→        if (!cell_in_bbox(cell_id, bbox)) continue;
  1675→        std::vector<uint32_t> new_ids;
  1676→        for (uint32_t id : ids) {
  1677→            auto it = interp_remap.find(id);
  1678→            if (it != interp_remap.end()) {
  1679→                new_ids.push_back(it->second);
  1680→            }
  1681→        }
  1682→        if (!new_ids.empty()) {
  1683→            out.cell_to_interps[cell_id] = std::move(new_ids);
  1684→        }
  1685→    }
  1686→
  1687→    for (const auto& [cell_id, ids] : full.cell_to_admin) {
  1688→        if (!cell_in_bbox(cell_id, bbox)) continue;
  1689→        std::vector<uint32_t> new_ids;
  1690→        for (uint32_t id : ids) {
  1691→            uint32_t raw_id = id & ID_MASK;
  1692→            uint32_t flags = id & INTERIOR_FLAG;
  1693→            auto it = admin_remap.find(raw_id);
  1694→            if (it != admin_remap.end()) {
  1695→                new_ids.push_back(it->second | flags);
  1696→            }
  1697→        }
  1698→        if (!new_ids.empty()) {
  1699→            out.cell_to_admin[cell_id] = std::move(new_ids);
  1700→        }
  1701→    }
  1702→
  1703→    // --- Rebuild compact string pool with only referenced strings ---
  1704→    // Collect all referenced string offsets
  1705→    std::unordered_set<uint32_t> used_offsets;
  1706→    for (const auto& w : out.ways) {
  1707→        used_offsets.insert(w.name_id);
  1708→    }
  1709→    for (const auto& a : out.addr_points) {
  1710→        used_offsets.insert(a.housenumber_id);
  1711→        used_offsets.insert(a.street_id);
  1712→    }
  1713→    for (const auto& iw : out.interp_ways) {
  1714→        used_offsets.insert(iw.street_id);
  1715→    }
  1716→    for (const auto& ap : out.admin_polygons) {
  1717→        used_offsets.insert(ap.name_id);
  1718→    }
  1719→
  1720→    // Build remap: old offset -> new offset
  1721→    const auto& old_sp = full.string_pool.data();
  1722→    std::unordered_map<uint32_t, uint32_t> string_remap;
  1723→    auto& new_sp = out.string_pool.mutable_data();
  1724→    new_sp.clear();
  1725→
  1726→    std::vector<uint32_t> sorted_offsets(used_offsets.begin(), used_offsets.end());
  1727→    std::sort(sorted_offsets.begin(), sorted_offsets.end());
  1728→
  1729→    for (uint32_t old_off : sorted_offsets) {
  1730→        uint32_t new_off = static_cast<uint32_t>(new_sp.size());
  1731→        string_remap[old_off] = new_off;
  1732→        // Copy the null-terminated string
  1733→        const char* str = old_sp.data() + old_off;
  1734→        size_t len = std::strlen(str);
  1735→        new_sp.insert(new_sp.end(), str, str + len + 1);
  1736→    }
  1737→
  1738→    // Remap all string IDs in the output data
  1739→    for (auto& w : out.ways) {
  1740→        w.name_id = string_remap[w.name_id];
  1741→    }
  1742→    for (auto& a : out.addr_points) {
  1743→        a.housenumber_id = string_remap[a.housenumber_id];
  1744→        a.street_id = string_remap[a.street_id];
  1745→    }
  1746→    for (auto& iw : out.interp_ways) {
  1747→        iw.street_id = string_remap[iw.street_id];
  1748→    }
  1749→    for (auto& ap : out.admin_polygons) {
  1750→        ap.name_id = string_remap[ap.name_id];
  1751→    }
  1752→
  1753→    return out;
  1754→}
  1755→
  1756→// --- Write cell index ---
  1757→
  1758→static const uint32_t NO_DATA = 0xFFFFFFFFu;
  1759→
  1760→// Write entries file from a hash map (used for addr and interp cell maps).
  1761→static std::vector<uint32_t> write_entries(
  1762→    const std::string& path,
  1763→    const std::vector<uint64_t>& sorted_cells,
  1764→    const std::unordered_map<uint64_t, std::vector<uint32_t>>& cell_map
  1765→) {
  1766→    struct CellRef { uint64_t cell_id; const std::vector<uint32_t>* ids; };
  1767→    std::vector<CellRef> sorted_refs;
  1768→    sorted_refs.reserve(cell_map.size());
  1769→    for (auto& [id, ids] : cell_map) {
  1770→        sorted_refs.push_back({id, &ids});
  1771→    }
  1772→    std::sort(sorted_refs.begin(), sorted_refs.end(),
  1773→        [](const CellRef& a, const CellRef& b) { return a.cell_id < b.cell_id; });
  1774→
  1775→    std::vector<uint32_t> offsets(sorted_cells.size(), NO_DATA);
  1776→    size_t total_size = 0;
  1777→    for (auto& r : sorted_refs) total_size += sizeof(uint16_t) + r.ids->size() * sizeof(uint32_t);
  1778→
  1779→    std::vector<char> buf;
  1780→    buf.reserve(total_size);
  1781→    uint32_t current = 0;
  1782→    size_t ri = 0;
  1783→    for (uint32_t si = 0; si < sorted_cells.size() && ri < sorted_refs.size(); si++) {
  1784→        if (sorted_cells[si] < sorted_refs[ri].cell_id) continue;
  1785→        if (sorted_cells[si] > sorted_refs[ri].cell_id) { si--; ri++; continue; }
  1786→        offsets[si] = current;
  1787→        const auto& ids = *sorted_refs[ri].ids;
  1788→        uint16_t count = static_cast<uint16_t>(std::min(ids.size(), size_t(65535)));
  1789→        buf.insert(buf.end(), reinterpret_cast<const char*>(&count),
  1790→                   reinterpret_cast<const char*>(&count) + sizeof(count));
  1791→        buf.insert(buf.end(), reinterpret_cast<const char*>(ids.data()),
  1792→                   reinterpret_cast<const char*>(ids.data()) + ids.size() * sizeof(uint32_t));
  1793→        current += sizeof(uint16_t) + ids.size() * sizeof(uint32_t);
  1794→        ri++;
  1795→    }
  1796→    std::ofstream f(path, std::ios::binary);
  1797→    f.write(buf.data(), buf.size());
  1798→    return offsets;
  1799→}
  1800→
  1801→// Write entries file from sorted pairs — parallel chunked merge-join.
  1802→// Splits sorted_cells into chunks, each thread builds its chunk independently,
  1803→// then prefix-sum computes global offsets and buffers are concatenated.
  1804→static std::vector<uint32_t> write_entries_from_sorted(
  1805→    const std::string& path,
  1806→    const std::vector<uint64_t>& sorted_cells,
  1807→    const std::vector<CellItemPair>& sorted_pairs
  1808→) {
  1809→    std::vector<uint32_t> offsets(sorted_cells.size(), NO_DATA);
  1810→    if (sorted_pairs.empty()) {
  1811→        std::ofstream f(path, std::ios::binary);
  1812→        return offsets;
  1813→    }
  1814→
  1815→    // Find where each chunk of sorted_cells starts in sorted_pairs
  1816→    // using binary search (both are sorted by cell_id)
  1817→    unsigned int nthreads = std::min(std::thread::hardware_concurrency(), 32u);
  1818→    if (nthreads == 0) nthreads = 4;
  1819→    size_t cells_per_chunk = (sorted_cells.size() + nthreads - 1) / nthreads;
  1820→
  1821→    struct ChunkResult {
  1822→        std::vector<char> buf;
  1823→        size_t cell_start, cell_end; // range in sorted_cells
  1824→        uint32_t local_size; // total bytes in this chunk's buffer
  1825→    };
  1826→    std::vector<ChunkResult> chunks(nthreads);
  1827→
  1828→    // Parallel: each thread processes its range of sorted_cells
  1829→    std::vector<std::thread> threads;
  1830→    for (unsigned int t = 0; t < nthreads; t++) {
  1831→        size_t cs = t * cells_per_chunk;
  1832→        size_t ce = std::min(cs + cells_per_chunk, sorted_cells.size());
  1833→        if (cs >= sorted_cells.size()) break;
  1834→
  1835→        threads.emplace_back([&, t, cs, ce]() {
  1836→            auto& chunk = chunks[t];
  1837→            chunk.cell_start = cs;
  1838→            chunk.cell_end = ce;
  1839→            chunk.local_size = 0;
  1840→
  1841→            // Binary search for where this chunk's first cell appears in sorted_pairs
  1842→            size_t pi = std::lower_bound(sorted_pairs.begin(), sorted_pairs.end(),
  1843→                sorted_cells[cs], [](const CellItemPair& p, uint64_t id) {
  1844→                    return p.cell_id < id;
  1845→                }) - sorted_pairs.begin();
  1846→
  1847→            // Merge-join for this chunk
  1848→            for (size_t si = cs; si < ce && pi < sorted_pairs.size(); si++) {
  1849→                if (sorted_cells[si] < sorted_pairs[pi].cell_id) continue;
  1850→                while (pi < sorted_pairs.size() && sorted_pairs[pi].cell_id < sorted_cells[si]) pi++;
  1851→                if (pi >= sorted_pairs.size() || sorted_pairs[pi].cell_id != sorted_cells[si]) continue;
  1852→
  1853→                offsets[si] = chunk.local_size; // local offset, adjusted later
  1854→                size_t start = pi;
  1855→                while (pi < sorted_pairs.size() && sorted_pairs[pi].cell_id == sorted_cells[si]) pi++;
  1856→                uint16_t count = static_cast<uint16_t>(std::min(pi - start, size_t(65535)));
  1857→                size_t entry_size = sizeof(uint16_t) + (pi - start) * sizeof(uint32_t);
  1858→                size_t buf_pos = chunk.buf.size();
  1859→                chunk.buf.resize(buf_pos + entry_size);
  1860→                memcpy(chunk.buf.data() + buf_pos, &count, sizeof(count));
  1861→                for (size_t k = start; k < pi; k++) {
  1862→                    memcpy(chunk.buf.data() + buf_pos + sizeof(uint16_t) + (k - start) * sizeof(uint32_t),
  1863→                           &sorted_pairs[k].item_id, sizeof(uint32_t));
  1864→                }
  1865→                chunk.local_size += entry_size;
  1866→            }
  1867→        });
  1868→    }
  1869→    for (auto& t : threads) t.join();
  1870→
  1871→    // Prefix-sum: compute global offsets for each chunk
  1872→    uint32_t global_offset = 0;
  1873→    for (auto& chunk : chunks) {
  1874→        for (size_t si = chunk.cell_start; si < chunk.cell_end; si++) {
  1875→            if (offsets[si] != NO_DATA) {
  1876→                offsets[si] += global_offset;
  1877→            }
  1878→        }
  1879→        global_offset += chunk.local_size;
  1880→    }
  1881→
  1882→    // Concatenate buffers and write
  1883→    std::vector<char> buf;
  1884→    buf.reserve(global_offset);
  1885→    for (auto& chunk : chunks) {
  1886→        buf.insert(buf.end(), chunk.buf.begin(), chunk.buf.end());
  1887→    }
  1888→
  1889→    std::ofstream f(path, std::ios::binary);
  1890→    f.write(buf.data(), buf.size());
  1891→    return offsets;
  1892→}
  1893→
  1894→static void write_cell_index(
  1895→    const std::string& cells_path,
  1896→    const std::string& entries_path,
  1897→    const std::unordered_map<uint64_t, std::vector<uint32_t>>& cell_map
  1898→) {
  1899→    std::vector<std::pair<uint64_t, std::vector<uint32_t>>> sorted(
  1900→        cell_map.begin(), cell_map.end());
  1901→    std::sort(sorted.begin(), sorted.end());
  1902→
  1903→    {
  1904→        std::ofstream f(cells_path, std::ios::binary);
  1905→        uint32_t current_offset = 0;
  1906→        for (const auto& [cell_id, ids] : sorted) {
  1907→            f.write(reinterpret_cast<const char*>(&cell_id), sizeof(cell_id));
  1908→            f.write(reinterpret_cast<const char*>(&current_offset), sizeof(current_offset));
  1909→            current_offset += sizeof(uint16_t) + ids.size() * sizeof(uint32_t);
  1910→        }
  1911→    }
  1912→
  1913→    {
  1914→        std::ofstream f(entries_path, std::ios::binary);
  1915→        for (const auto& [cell_id, ids] : sorted) {
  1916→            uint16_t count = static_cast<uint16_t>(std::min(ids.size(), size_t(65535)));
  1917→            f.write(reinterpret_cast<const char*>(&count), sizeof(count));
  1918→            f.write(reinterpret_cast<const char*>(ids.data()), ids.size() * sizeof(uint32_t));
  1919→        }
  1920→    }
  1921→}
  1922→
  1923→// --- Write all index files ---
  1924→
  1925→static void write_index(const ParsedData& data, const std::string& output_dir, IndexMode mode) {
  1926→    ensure_dir(output_dir);
  1927→    auto _wt = std::chrono::steady_clock::now();
  1928→
  1929→    bool write_streets = (mode != IndexMode::AdminOnly);
  1930→    bool write_addresses = (mode == IndexMode::Full);
  1931→
  1932→    if (write_streets) {
  1933→        // Build sorted_geo_cells from sorted pairs where available (avoids hash map iteration).
  1934→        // Extract unique cell IDs from each sorted pair vector in parallel, then merge.
  1935→        std::vector<uint64_t> sorted_geo_cells;
  1936→        {
  1937→            auto extract_unique_cells = [](const std::vector<CellItemPair>& pairs) {
  1938→                std::vector<uint64_t> cells;
  1939→                cells.reserve(pairs.size() / 2);
  1940→                for (size_t i = 0; i < pairs.size(); ) {
  1941→                    cells.push_back(pairs[i].cell_id);
  1942→                    uint64_t cur = pairs[i].cell_id;
  1943→                    while (i < pairs.size() && pairs[i].cell_id == cur) i++;
  1944→                }
  1945→                return cells;
  1946→            };
  1947→            auto extract_from_map = [](const std::unordered_map<uint64_t, std::vector<uint32_t>>& m) {
  1948→                std::vector<uint64_t> cells;
  1949→                cells.reserve(m.size());
  1950→                for (auto& [id, _] : m) cells.push_back(id);
  1951→                std::sort(cells.begin(), cells.end());
  1952→                return cells;
  1953→            };
  1954→
  1955→            // Extract unique cell IDs in parallel
  1956→            std::vector<uint64_t> way_cells, addr_cells, interp_cells;
  1957→            {
  1958→                auto f1 = std::async(std::launch::async, [&] {
  1959→                    return !data.sorted_way_cells.empty()
  1960→                        ? extract_unique_cells(data.sorted_way_cells)
  1961→                        : extract_from_map(data.cell_to_ways);
  1962→                });
  1963→                if (write_addresses) {
  1964→                    auto f2 = std::async(std::launch::async, [&] {
  1965→                        return !data.sorted_addr_cells.empty()
  1966→                            ? extract_unique_cells(data.sorted_addr_cells)
  1967→                            : extract_from_map(data.cell_to_addrs);
  1968→                    });
  1969→                    auto f3 = std::async(std::launch::async, [&] {
  1970→                        return extract_from_map(data.cell_to_interps);
  1971→                    });
  1972→                    addr_cells = f2.get();
  1973→                    interp_cells = f3.get();
  1974→                }
  1975→                way_cells = f1.get();
  1976→            }
  1977→
  1978→            // Merge sorted unique cell ID vectors
  1979→            sorted_geo_cells.reserve(way_cells.size() + addr_cells.size() + interp_cells.size());
  1980→            std::merge(way_cells.begin(), way_cells.end(),
  1981→                       addr_cells.begin(), addr_cells.end(),
  1982→                       std::back_inserter(sorted_geo_cells));
  1983→            if (!interp_cells.empty()) {
  1984→                std::vector<uint64_t> tmp;
  1985→                tmp.reserve(sorted_geo_cells.size() + interp_cells.size());
  1986→                std::merge(sorted_geo_cells.begin(), sorted_geo_cells.end(),
  1987→                           interp_cells.begin(), interp_cells.end(),
  1988→                           std::back_inserter(tmp));
  1989→                sorted_geo_cells = std::move(tmp);
  1990→            }
  1991→            // Deduplicate (merge may have duplicates from overlapping cells)
  1992→            sorted_geo_cells.erase(std::unique(sorted_geo_cells.begin(), sorted_geo_cells.end()),
  1993→                                    sorted_geo_cells.end());
  1994→        }
  1995→
  1996→        // Write entry files in parallel (returns positional offset vectors, not hash maps)
  1997→        std::vector<uint32_t> street_offsets, addr_offsets, interp_offsets;
  1998→        {
  1999→            auto f1 = std::async(std::launch::async, [&]() {
  2000→                // Use direct sorted-pair writing for ways (skips hash map extraction + sort)
  2001→                if (!data.sorted_way_cells.empty()) {
  2002→                    return write_entries_from_sorted(output_dir + "/street_entries.bin", sorted_geo_cells, data.sorted_way_cells);
  2003→                }
  2004→                return write_entries(output_dir + "/street_entries.bin", sorted_geo_cells, data.cell_to_ways);
  2005→            });
  2006→            if (write_addresses) {
  2007→                auto f2 = std::async(std::launch::async, [&]() {
  2008→                    if (!data.sorted_addr_cells.empty()) {
  2009→                        return write_entries_from_sorted(output_dir + "/addr_entries.bin", sorted_geo_cells, data.sorted_addr_cells);
  2010→                    }
  2011→                    return write_entries(output_dir + "/addr_entries.bin", sorted_geo_cells, data.cell_to_addrs);
  2012→                });
  2013→                auto f3 = std::async(std::launch::async, [&]() {
  2014→                    return write_entries(output_dir + "/interp_entries.bin", sorted_geo_cells, data.cell_to_interps);
  2015→                });
  2016→                addr_offsets = f2.get();
  2017→                interp_offsets = f3.get();
  2018→            }
  2019→            street_offsets = f1.get();
  2020→        }
  2021→
  2022→        log_phase("  Write: entry files", _wt);
  2023→        // Build geo_cells.bin — parallel buffer fill + single write
  2024→        {
  2025→            size_t n = sorted_geo_cells.size();
  2026→            size_t row_size = sizeof(uint64_t) + 3 * sizeof(uint32_t);
  2027→            std::vector<char> buf(n * row_size);
  2028→
  2029→            if (addr_offsets.empty()) addr_offsets.resize(n, NO_DATA);
  2030→            if (interp_offsets.empty()) interp_offsets.resize(n, NO_DATA);
  2031→
  2032→            // Parallel buffer fill — split into chunks across threads
  2033→            unsigned int nthreads = std::thread::hardware_concurrency();
  2034→            if (nthreads == 0) nthreads = 4;
  2035→            size_t chunk = (n + nthreads - 1) / nthreads;
  2036→            std::vector<std::thread> fill_threads;
  2037→            for (unsigned int t = 0; t < nthreads; t++) {
  2038→                size_t start = t * chunk;
  2039→                size_t end = std::min(start + chunk, n);
  2040→                if (start >= n) break;
  2041→                fill_threads.emplace_back([&, start, end]() {
  2042→                    char* ptr = buf.data() + start * row_size;
  2043→                    for (size_t i = start; i < end; i++) {
  2044→                        memcpy(ptr, &sorted_geo_cells[i], sizeof(uint64_t)); ptr += sizeof(uint64_t);
  2045→                        memcpy(ptr, &street_offsets[i], sizeof(uint32_t)); ptr += sizeof(uint32_t);
  2046→                        memcpy(ptr, &addr_offsets[i], sizeof(uint32_t)); ptr += sizeof(uint32_t);
  2047→                        memcpy(ptr, &interp_offsets[i], sizeof(uint32_t)); ptr += sizeof(uint32_t);
  2048→                    }
  2049→                });
  2050→            }
  2051→            for (auto& t : fill_threads) t.join();
  2052→
  2053→            std::ofstream f(output_dir + "/geo_cells.bin", std::ios::binary);
  2054→            f.write(buf.data(), buf.size());
  2055→        }
  2056→
  2057→        std::cerr << "geo index: " << sorted_geo_cells.size() << " cells ("
  2058→                  << data.ways.size() << " ways, "
  2059→                  << data.addr_points.size() << " addrs, "
  2060→                  << data.interp_ways.size() << " interps)" << std::endl;
  2061→    }
  2062→
  2063→    log_phase("  Write: geo_cells.bin", _wt);
  2064→    // Write admin cell index, raw data files, and admin polygons in parallel
  2065→    auto admin_future = std::async(std::launch::async, [&] {
  2066→        write_cell_index(output_dir + "/admin_cells.bin", output_dir + "/admin_entries.bin", data.cell_to_admin);
  2067→        std::cerr << "admin index: " << data.cell_to_admin.size() << " cells, " << data.admin_polygons.size() << " polygons" << std::endl;
  2068→    });
  2069→
  2070→    std::vector<std::future<void>> write_futures;
  2071→
  2072→    if (write_streets) {
  2073→        write_futures.push_back(std::async(std::launch::async, [&] {
  2074→            std::ofstream f(output_dir + "/street_ways.bin", std::ios::binary);
  2075→            f.write(reinterpret_cast<const char*>(data.ways.data()), data.ways.size() * sizeof(WayHeader));
  2076→        }));
  2077→        write_futures.push_back(std::async(std::launch::async, [&] {
  2078→            std::ofstream f(output_dir + "/street_nodes.bin", std::ios::binary);
  2079→            f.write(reinterpret_cast<const char*>(data.street_nodes.data()), data.street_nodes.size() * sizeof(NodeCoord));
  2080→        }));
  2081→        if (write_addresses) {
  2082→            write_futures.push_back(std::async(std::launch::async, [&] {
  2083→                std::ofstream f(output_dir + "/addr_points.bin", std::ios::binary);
  2084→                f.write(reinterpret_cast<const char*>(data.addr_points.data()), data.addr_points.size() * sizeof(AddrPoint));
  2085→            }));
  2086→            write_futures.push_back(std::async(std::launch::async, [&] {
  2087→                std::ofstream f(output_dir + "/interp_ways.bin", std::ios::binary);
  2088→                f.write(reinterpret_cast<const char*>(data.interp_ways.data()), data.interp_ways.size() * sizeof(InterpWay));
  2089→            }));
  2090→            write_futures.push_back(std::async(std::launch::async, [&] {
  2091→                std::ofstream f(output_dir + "/interp_nodes.bin", std::ios::binary);
  2092→                f.write(reinterpret_cast<const char*>(data.interp_nodes.data()), data.interp_nodes.size() * sizeof(NodeCoord));
  2093→            }));
  2094→        }
  2095→    }
  2096→


  2096→
  2097→    write_futures.push_back(std::async(std::launch::async, [&] {
  2098→        std::ofstream f(output_dir + "/admin_polygons.bin", std::ios::binary);
  2099→        f.write(reinterpret_cast<const char*>(data.admin_polygons.data()), data.admin_polygons.size() * sizeof(AdminPolygon));
  2100→    }));
  2101→
  2102→    write_futures.push_back(std::async(std::launch::async, [&] {
  2103→        std::ofstream f(output_dir + "/admin_vertices.bin", std::ios::binary);
  2104→        f.write(reinterpret_cast<const char*>(data.admin_vertices.data()), data.admin_vertices.size() * sizeof(NodeCoord));
  2105→    }));
  2106→
  2107→    write_futures.push_back(std::async(std::launch::async, [&] {
  2108→        std::ofstream f(output_dir + "/strings.bin", std::ios::binary);
  2109→        f.write(data.string_pool.data().data(), data.string_pool.data().size());
  2110→        std::cerr << "strings.bin: " << data.string_pool.data().size() << " bytes" << std::endl;
  2111→    }));
  2112→
  2113→    // Wait for all parallel writes
  2114→    log_phase("  Write: parallel data files launched", _wt);
  2115→    admin_future.get();
  2116→    for (auto& f : write_futures) f.get();
  2117→}
  2118→
  2119→// --- Main ---
  2120→
  2121→int main(int argc, char* argv[]) {
  2122→    if (argc < 2) {
  2123→        std::cerr << "Usage: build-index <output-dir> <input.osm.pbf> [options]" << std::endl;
  2124→        std::cerr << "       build-index <output-dir> --load-cache <path> [options]" << std::endl;
  2125→        std::cerr << std::endl;
  2126→        std::cerr << "Options:" << std::endl;
  2127→        std::cerr << "  --street-level N       S2 cell level for streets (default: 17)" << std::endl;
  2128→        std::cerr << "  --admin-level N        S2 cell level for admin (default: 10)" << std::endl;
  2129→        std::cerr << "  --max-admin-level N    Only include admin levels <= N" << std::endl;
  2130→        std::cerr << "  --save-cache <path>    Save parsed data to cache file" << std::endl;
  2131→        std::cerr << "  --load-cache <path>    Load from cache instead of PBF" << std::endl;
  2132→        std::cerr << "  --multi-output         Write full, no-addresses, admin-only indexes" << std::endl;
  2133→        std::cerr << "  --continents           Also generate per-continent indexes" << std::endl;
  2134→        std::cerr << "  --mode <mode>          Index mode: full, no-addresses, admin-only (default: full)" << std::endl;
  2135→        std::cerr << "  --admin-only           Shorthand for --mode admin-only" << std::endl;
  2136→        std::cerr << "  --no-addresses         Shorthand for --mode no-addresses" << std::endl;
  2137→        std::cerr << "  --parallel-admin       Use parallel admin boundary assembly (default: on)" << std::endl;
  2138→        std::cerr << "  --no-parallel-admin    Disable parallel admin assembly (use osmium fallback)" << std::endl;
  2139→        return 1;
  2140→    }
  2141→
  2142→    std::string output_dir = argv[1];
  2143→    std::vector<std::string> input_files;
  2144→    std::string save_cache_path;
  2145→    std::string load_cache_path;
  2146→    bool multi_output = false;
  2147→    bool generate_continents = false;
  2148→    bool parallel_admin = true;
  2149→    IndexMode mode = IndexMode::Full;
  2150→
  2151→    for (int i = 2; i < argc; i++) {
  2152→        std::string arg = argv[i];
  2153→        if (arg == "--street-level" && i + 1 < argc) {
  2154→            kStreetCellLevel = std::atoi(argv[++i]);
  2155→        } else if (arg == "--admin-level" && i + 1 < argc) {
  2156→            kAdminCellLevel = std::atoi(argv[++i]);
  2157→        } else if (arg == "--admin-only") {
  2158→            mode = IndexMode::AdminOnly;
  2159→        } else if (arg == "--no-addresses") {
  2160→            mode = IndexMode::NoAddresses;
  2161→        } else if (arg == "--max-admin-level" && i + 1 < argc) {
  2162→            kMaxAdminLevel = std::atoi(argv[++i]);
  2163→        } else if (arg == "--save-cache" && i + 1 < argc) {
  2164→            save_cache_path = argv[++i];
  2165→        } else if (arg == "--load-cache" && i + 1 < argc) {
  2166→            load_cache_path = argv[++i];
  2167→        } else if (arg == "--multi-output") {
  2168→            multi_output = true;
  2169→        } else if (arg == "--continents") {
  2170→            generate_continents = true;
  2171→        } else if (arg == "--parallel-admin") {
  2172→            parallel_admin = true;
  2173→        } else if (arg == "--no-parallel-admin") {
  2174→            parallel_admin = false;
  2175→        } else if (arg == "--mode" && i + 1 < argc) {
  2176→            std::string mode_str = argv[++i];
  2177→            if (mode_str == "full") {
  2178→                mode = IndexMode::Full;
  2179→            } else if (mode_str == "no-addresses") {
  2180→                mode = IndexMode::NoAddresses;
  2181→            } else if (mode_str == "admin-only") {
  2182→                mode = IndexMode::AdminOnly;
  2183→            } else {
  2184→                std::cerr << "Error: unknown mode '" << mode_str << "'" << std::endl;
  2185→                return 1;
  2186→            }
  2187→        } else {
  2188→            input_files.push_back(arg);
  2189→        }
  2190→    }
  2191→
  2192→    ParsedData data;
  2193→    auto _pt = std::chrono::steady_clock::now();
  2194→
  2195→    if (!load_cache_path.empty()) {
  2196→        // Load from cache
  2197→        if (!deserialize_cache(data, load_cache_path)) {
  2198→            std::cerr << "Error: failed to load cache" << std::endl;
  2199→            return 1;
  2200→        }
  2201→    } else {
  2202→        // Parse PBF files (always collect everything)
  2203→        if (input_files.empty()) {
  2204→            std::cerr << "Error: no input files specified and no --load-cache" << std::endl;
  2205→            return 1;
  2206→        }
  2207→
  2208→        // Create thread pool for concurrent admin polygon S2 covering
  2209→        unsigned int num_threads = std::max(1u, std::thread::hardware_concurrency() > 4 ? std::thread::hardware_concurrency() - 4 : 1u);
  2210→        std::cerr << "Using " << num_threads << " worker threads." << std::endl;
  2211→        AdminCoverPool admin_pool(num_threads);
  2212→        BuildHandler handler(data, &admin_pool);
  2213→
  2214→        // Dense array node location index — lockless parallel writes
  2215→        // Planet OSM node IDs max ~12.5 billion. 8 bytes per Location = 100GB virtual.
  2216→        // MAP_NORESERVE means OS only allocates pages on write (~80GB for 10B nodes).
  2217→        static const size_t MAX_NODE_ID = 15000000000ULL;
  2218→        struct DenseIndex {
  2219→            osmium::Location* data;
  2220→            size_t capacity;
  2221→
  2222→            DenseIndex() : capacity(MAX_NODE_ID) {
  2223→                void* ptr = mmap(nullptr, capacity * sizeof(osmium::Location),
  2224→                    PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_NORESERVE,
  2225→                    -1, 0);
  2226→                if (ptr == MAP_FAILED) {
  2227→                    std::cerr << "Error: failed to mmap dense node index ("
  2228→                              << (capacity * sizeof(osmium::Location) / (1024*1024*1024)) << "GB virtual)" << std::endl;
  2229→                    std::exit(1);
  2230→                }
  2231→                data = static_cast<osmium::Location*>(ptr);
  2232→                std::cerr << "Allocated dense node index: " << (capacity * sizeof(osmium::Location) / (1024*1024*1024))
  2233→                          << "GB virtual address space" << std::endl;
  2234→            }
  2235→
  2236→            ~DenseIndex() {
  2237→                munmap(data, capacity * sizeof(osmium::Location));
  2238→            }
  2239→
  2240→            // Lockless — each node ID maps to a unique array slot
  2241→            void set(osmium::unsigned_object_id_type id, osmium::Location loc) {
  2242→                if (id >= capacity) {
  2243→                    std::cerr << "FATAL: node ID " << id << " exceeds dense index capacity " << capacity << std::endl;
  2244→                    std::exit(1);
  2245→                }
  2246→                data[id] = loc;
  2247→            }
  2248→
  2249→            osmium::Location get(osmium::unsigned_object_id_type id) const {
  2250→                if (id >= capacity) return osmium::Location{};
  2251→                return data[id];
  2252→            }
  2253→
  2254→            void set_batch(const std::vector<std::pair<osmium::unsigned_object_id_type, osmium::Location>>& batch) {
  2255→                for (const auto& [id, loc] : batch) {
  2256→                    if (id >= capacity) {
  2257→                        std::cerr << "FATAL: node ID " << id << " exceeds dense index capacity " << capacity << std::endl;
  2258→                        std::exit(1);
  2259→                    }
  2260→                    data[id] = loc;
  2261→                }
  2262→            }
  2263→        } index;
  2264→
  2265→        for (const auto& input_file : input_files) {
  2266→            std::cerr << "Processing " << input_file << "..." << std::endl;
  2267→
  2268→            // --- Pass 1: collect relation members for multipolygon assembly ---
  2269→            std::cerr << "  Pass 1: scanning relations..." << std::endl;
  2270→
  2271→            osmium::area::Assembler::config_type assembler_config;
  2272→            osmium::area::MultipolygonManager<osmium::area::Assembler> mp_manager{assembler_config};
  2273→
  2274→            // Also collect admin/postal relation metadata for parallel assembly
  2275→            RelationCollector rel_collector(data.collected_relations);
  2276→
  2277→            {
  2278→                osmium::io::Reader reader1{input_file, osmium::osm_entity_bits::relation};
  2279→                if (parallel_admin) {
  2280→                    // Run both: mp_manager (for fallback compatibility) and rel_collector
  2281→                    osmium::apply(reader1, mp_manager, rel_collector);
  2282→                } else {
  2283→                    osmium::apply(reader1, mp_manager);
  2284→                }
  2285→                reader1.close();
  2286→                mp_manager.prepare_for_lookup();
  2287→            }
  2288→            if (parallel_admin) {
  2289→                std::cerr << "  Collected " << data.collected_relations.size()
  2290→                          << " admin/postal relations for parallel assembly." << std::endl;
  2291→            }
  2292→
  2293→            // --- Combined Pass 2+3: nodes then ways in a single PBF read ---
  2294→            // PBF guarantees ordering: nodes → ways → relations.
  2295→            // We read everything in one pass, processing nodes first, then
  2296→            // transitioning to way processing when the first way block arrives.
  2297→            log_phase("Pass 1: relation scanning", _pt);
  2298→
  2299→            // Build admin_way_ids BEFORE the combined pass (needed during way processing)
  2300→            std::unordered_set<int64_t> admin_way_ids;
  2301→            if (parallel_admin) {
  2302→                for (const auto& rel : data.collected_relations) {
  2303→                    for (const auto& [way_id, role] : rel.members) {
  2304→                        admin_way_ids.insert(way_id);
  2305→                    }
  2306→                }
  2307→                std::cerr << "  Admin assembly needs " << admin_way_ids.size() << " way geometries." << std::endl;
  2308→            }
  2309→
  2310→            std::cerr << "  Pass 2+3: processing nodes and ways in single read..." << std::endl;
  2311→
  2312→            {
  2313→                // Shared queue infrastructure for both phases
  2314→                std::mutex queue_mutex;
  2315→                std::condition_variable queue_cv;
  2316→                std::deque<osmium::memory::Buffer> block_queue;
  2317→                bool reader_done = false;
  2318→                const size_t MAX_QUEUE = 64;
  2319→
  2320→                struct NodeThreadLocal {
  2321→                    std::vector<std::pair<double,double>> addr_coords;
  2322→                    std::vector<std::pair<std::string,std::string>> addr_strings;
  2323→                    uint64_t count = 0;
  2324→                };
  2325→                std::vector<NodeThreadLocal> ntld(num_threads);
  2326→                std::atomic<uint64_t> blocks_done{0};
  2327→
  2328→                // Worker threads
  2329→                std::vector<std::thread> node_workers;
  2330→                for (unsigned int t = 0; t < num_threads; t++) {
  2331→                    node_workers.emplace_back([&, t]() {
  2332→                        auto& local = ntld[t];
  2333→                        std::vector<std::pair<osmium::unsigned_object_id_type, osmium::Location>> loc_batch;
  2334→                        loc_batch.reserve(8192);
  2335→
  2336→                        while (true) {
  2337→                            osmium::memory::Buffer block;
  2338→                            {
  2339→                                std::unique_lock<std::mutex> lock(queue_mutex);
  2340→                                queue_cv.wait(lock, [&]{ return !block_queue.empty() || reader_done; });
  2341→                                if (block_queue.empty() && reader_done) break;
  2342→                                block = std::move(block_queue.front());
  2343→                                block_queue.pop_front();
  2344→                            }
  2345→                            queue_cv.notify_one(); // wake reader if it was waiting
  2346→
  2347→                            for (const auto& item : block) {
  2348→                                if (item.type() == osmium::item_type::node) {
  2349→                                    const auto& node = static_cast<const osmium::Node&>(item);
  2350→                                    if (!node.location().valid()) continue;
  2351→                                    loc_batch.push_back({node.positive_id(), node.location()});
  2352→
  2353→                                    const char* housenumber = node.tags()["addr:housenumber"];
  2354→                                    if (housenumber) {
  2355→                                        const char* street = node.tags()["addr:street"];
  2356→                                        if (street) {
  2357→                                            local.addr_coords.push_back({node.location().lat(), node.location().lon()});
  2358→                                            local.addr_strings.push_back({housenumber, street});
  2359→                                            local.count++;
  2360→                                        }
  2361→                                    }
  2362→                                }
  2363→                            }
  2364→
  2365→                            // Flush location batch (sharded index handles its own locking)
  2366→                            if (!loc_batch.empty()) {
  2367→                                index.set_batch(loc_batch);
  2368→                                loc_batch.clear();
  2369→                            }
  2370→
  2371→                            uint64_t n = blocks_done.fetch_add(1) + 1;
  2372→                            if (n % 1000 == 0) {
  2373→                                std::cerr << "  Processed " << n << " node blocks..." << std::endl;
  2374→                            }
  2375→                        }
  2376→
  2377→                        // Flush remaining
  2378→                        if (!loc_batch.empty()) {
  2379→                            index.set_batch(loc_batch);
  2380→                        }
  2381→                    });
  2382→                }
  2383→
  2384→                // Single reader for all entity types — processes nodes first,
  2385→                // then transitions to way processing when first way block arrives.
  2386→                osmium::io::Reader combined_reader{input_file};
  2387→                bool nodes_done = false;
  2388→                osmium::memory::Buffer first_way_buf; // stash the first way block
  2389→
  2390→                // Phase 1: Feed node blocks to node workers
  2391→                while (auto buf = combined_reader.read()) {
  2392→                    // Check if this buffer contains nodes or ways/relations
  2393→                    bool has_node = false;
  2394→                    for (const auto& item : buf) {
  2395→                        if (item.type() == osmium::item_type::node) { has_node = true; break; }
  2396→                    }
  2397→
  2398→                    if (has_node) {
  2399→                        std::unique_lock<std::mutex> lock(queue_mutex);
  2400→                        queue_cv.wait(lock, [&]{ return block_queue.size() < MAX_QUEUE; });
  2401→                        block_queue.push_back(std::move(buf));
  2402→                        queue_cv.notify_one();
  2403→                    } else {
  2404→                        // First non-node block — stash it and transition
  2405→                        first_way_buf = std::move(buf);
  2406→                        nodes_done = true;
  2407→                        break;
  2408→                    }
  2409→                }
  2410→
  2411→                // Signal node workers done
  2412→                {
  2413→                    std::lock_guard<std::mutex> lock(queue_mutex);
  2414→                    reader_done = true;
  2415→                }
  2416→                queue_cv.notify_all();
  2417→                for (auto& w : node_workers) w.join();
  2418→
  2419→                // Merge address points
  2420→                uint64_t total_addrs = 0;
  2421→                for (auto& local : ntld) {
  2422→                    for (size_t j = 0; j < local.addr_coords.size(); j++) {
  2423→                        uint64_t dummy = 0;
  2424→                        add_addr_point(data, local.addr_coords[j].first, local.addr_coords[j].second,
  2425→                                       local.addr_strings[j].first.c_str(),
  2426→                                       local.addr_strings[j].second.c_str(), dummy);
  2427→                    }
  2428→                    total_addrs += local.count;
  2429→                }
  2430→                std::cerr << "  Node processing complete: " << total_addrs
  2431→                          << " address points collected." << std::endl;
  2432→                log_phase("Pass 2: node processing", _pt);
  2433→                std::cerr << "  Processing ways with " << num_threads << " threads..." << std::endl;
  2434→
  2435→                // --- Phase 2: Way processing using same reader ---
  2436→
  2437→                // Thread-local data for parallel way processing
  2438→                struct ThreadLocalData {
  2439→                    std::vector<WayHeader> ways;
  2440→                    std::vector<NodeCoord> street_nodes;
  2441→                    std::vector<DeferredWay> deferred_ways;
  2442→                    std::vector<InterpWay> interp_ways;
  2443→                    std::vector<NodeCoord> interp_nodes;
  2444→                    std::vector<DeferredInterp> deferred_interps;
  2445→                    std::vector<AddrPoint> building_addrs;
  2446→                    std::vector<std::pair<double, double>> building_addr_coords; // lat,lng for S2 cell
  2447→                    std::vector<std::string> way_strings;      // way name strings
  2448→                    std::vector<std::pair<std::string,std::string>> addr_strings; // building addr {hn, street}
  2449→                    std::vector<std::string> interp_strings;   // interp street name strings
  2450→                    uint64_t way_count = 0;
  2451→                    uint64_t building_addr_count = 0;
  2452→                    uint64_t interp_count = 0;
  2453→                    // Way geometries for parallel admin assembly
  2454→                    struct WayGeomEntry {
  2455→                        int64_t way_id;
  2456→                        std::vector<std::pair<double,double>> coords;
  2457→                        int64_t first_node_id;
  2458→                        int64_t last_node_id;
  2459→                    };
  2460→                    std::vector<WayGeomEntry> way_geoms;
  2461→                    // Closed-way admin polygons (ways with boundary=administrative)
  2462→                    struct ClosedWayAdmin {
  2463→                        std::vector<std::pair<double,double>> vertices;
  2464→                        std::string name;
  2465→                        uint8_t admin_level;
  2466→                        std::string country_code;
  2467→                    };
  2468→                    std::vector<ClosedWayAdmin> closed_way_admins;
  2469→                };
  2470→
  2471→                // Read buffers and dispatch to thread pool
  2472→                // Streaming producer-consumer for way blocks
  2473→                std::mutex way_queue_mutex;
  2474→                std::condition_variable way_queue_cv;
  2475→                std::deque<osmium::memory::Buffer> way_block_queue;
  2476→                bool way_reader_done = false;
  2477→                const size_t WAY_MAX_QUEUE = 64;
  2478→
  2479→                std::vector<ThreadLocalData> tld(num_threads);
  2480→                std::atomic<uint64_t> way_blocks_done{0};
  2481→
  2482→                std::vector<std::thread> workers;
  2483→                for (unsigned int t = 0; t < num_threads; t++) {
  2484→                    workers.emplace_back([&, t]() {
  2485→                        auto& local = tld[t];
  2486→                        while (true) {
  2487→                            osmium::memory::Buffer block;
  2488→                            {
  2489→                                std::unique_lock<std::mutex> lock(way_queue_mutex);
  2490→                                way_queue_cv.wait(lock, [&]{ return !way_block_queue.empty() || way_reader_done; });
  2491→                                if (way_block_queue.empty() && way_reader_done) break;
  2492→                                block = std::move(way_block_queue.front());
  2493→                                way_block_queue.pop_front();
  2494→                            }
  2495→                            way_queue_cv.notify_one();
  2496→
  2497→                            for (const auto& item : block) {
  2498→                                if (item.type() == osmium::item_type::way) {
  2499→                                    const auto& way = static_cast<const osmium::Way&>(item);
  2500→
  2501→                                    // Resolve node locations (read-only from shared index)
  2502→                                    // We need to check if nodes have valid locations
  2503→                                    const auto& wnodes = way.nodes();
  2504→
  2505→                                    // Address interpolation
  2506→                                    const char* interpolation = way.tags()["addr:interpolation"];
  2507→                                    if (interpolation) {
  2508→                                        if (wnodes.size() >= 2) {
  2509→                                            bool all_valid = true;
  2510→                                            for (const auto& nr : wnodes) {
  2511→                                                try {
  2512→                                                    auto loc = index.get(nr.positive_ref());
  2513→                                                    if (!loc.valid()) { all_valid = false; break; }
  2514→                                                } catch (...) { all_valid = false; break; }
  2515→                                            }
  2516→                                            if (all_valid) {
  2517→                                                const char* street = way.tags()["addr:street"];
  2518→                                                if (street) {
  2519→                                                    uint32_t interp_id = static_cast<uint32_t>(local.interp_ways.size());
  2520→                                                    uint32_t node_offset = static_cast<uint32_t>(local.interp_nodes.size());
  2521→
  2522→                                                    for (const auto& nr : wnodes) {
  2523→                                                        auto loc = index.get(nr.positive_ref());
  2524→                                                        local.interp_nodes.push_back({
  2525→                                                            static_cast<float>(loc.lat()),
  2526→                                                            static_cast<float>(loc.lon())
  2527→                                                        });
  2528→                                                    }
  2529→
  2530→                                                    uint8_t interp_type = 0;
  2531→                                                    if (std::strcmp(interpolation, "even") == 0) interp_type = 1;
  2532→                                                    else if (std::strcmp(interpolation, "odd") == 0) interp_type = 2;
  2533→
  2534→                                                    InterpWay iw{};
  2535→                                                    iw.node_offset = node_offset;
  2536→                                                    iw.node_count = static_cast<uint8_t>(std::min(wnodes.size(), size_t(255)));
  2537→                                                    iw.street_id = 0; // placeholder, intern later
  2538→                                                    iw.start_number = 0;
  2539→                                                    iw.end_number = 0;
  2540→                                                    iw.interpolation = interp_type;
  2541→                                                    local.interp_ways.push_back(iw);
  2542→                                                    local.interp_strings.push_back(street);
  2543→                                                    local.deferred_interps.push_back({interp_id, node_offset, iw.node_count});
  2544→                                                    local.interp_count++;
  2545→                                                }
  2546→                                            }
  2547→                                        }
  2548→                                        continue;
  2549→                                    }
  2550→
  2551→                                    // Building addresses
  2552→                                    const char* housenumber = way.tags()["addr:housenumber"];
  2553→                                    if (housenumber) {
  2554→                                        const char* street = way.tags()["addr:street"];
  2555→                                        if (street && !wnodes.empty()) {
  2556→                                            double sum_lat = 0, sum_lng = 0;
  2557→                                            int valid = 0;
  2558→                                            for (const auto& nr : wnodes) {
  2559→                                                try {
  2560→                                                    auto loc = index.get(nr.positive_ref());
  2561→                                                    if (loc.valid()) {
  2562→                                                        sum_lat += loc.lat();
  2563→                                                        sum_lng += loc.lon();
  2564→                                                        valid++;
  2565→                                                    }
  2566→                                                } catch (...) {}
  2567→                                            }
  2568→                                            if (valid > 0) {
  2569→                                                // Store coords for later S2 cell computation
  2570→                                                local.building_addr_coords.push_back({sum_lat / valid, sum_lng / valid});
  2571→                                                // Placeholder addr point — string IDs filled in during merge
  2572→                                                local.building_addrs.push_back({
  2573→                                                    static_cast<float>(sum_lat / valid),
  2574→                                                    static_cast<float>(sum_lng / valid),
  2575→                                                    0, 0 // placeholder string IDs
  2576→                                                });
  2577→                                                // Store strings for later interning
  2578→                                                local.addr_strings.push_back({housenumber, street});
  2579→                                                local.building_addr_count++;
  2580→                                            }
  2581→                                        }
  2582→                                    }
  2583→
  2584→                                    // Highway ways
  2585→                                    const char* highway = way.tags()["highway"];
  2586→                                    if (highway && is_included_highway(highway)) {
  2587→                                        const char* name = way.tags()["name"];
  2588→                                        if (name && wnodes.size() >= 2) {
  2589→                                            bool all_valid = true;
  2590→                                            for (const auto& nr : wnodes) {
  2591→                                                try {
  2592→                                                    auto loc = index.get(nr.positive_ref());
  2593→                                                    if (!loc.valid()) { all_valid = false; break; }
  2594→                                                } catch (...) { all_valid = false; break; }
  2595→                                            }


  2595→                                            }
  2596→                                            if (all_valid) {
  2597→                                                uint32_t way_id = static_cast<uint32_t>(local.ways.size());
  2598→                                                uint32_t node_offset = static_cast<uint32_t>(local.street_nodes.size());
  2599→
  2600→                                                for (const auto& nr : wnodes) {
  2601→                                                    auto loc = index.get(nr.positive_ref());
  2602→                                                    local.street_nodes.push_back({
  2603→                                                        static_cast<float>(loc.lat()),
  2604→                                                        static_cast<float>(loc.lon())
  2605→                                                    });
  2606→                                                }
  2607→
  2608→                                                WayHeader header{};
  2609→                                                header.node_offset = node_offset;
  2610→                                                header.node_count = static_cast<uint8_t>(std::min(wnodes.size(), size_t(255)));
  2611→                                                header.name_id = 0; // placeholder, intern later
  2612→                                                local.ways.push_back(header);
  2613→                                                local.way_strings.push_back(name);
  2614→                                                local.deferred_ways.push_back({way_id, node_offset, header.node_count});
  2615→                                                local.way_count++;
  2616→                                            }
  2617→                                        }
  2618→                                    }
  2619→
  2620→                                    // Store way geometry only for admin boundary member ways
  2621→                                    if (parallel_admin && !wnodes.empty() && admin_way_ids.count(way.id())) {
  2622→                                        std::vector<std::pair<double,double>> geom;
  2623→                                        for (const auto& nr : wnodes) {
  2624→                                            try {
  2625→                                                auto loc = index.get(nr.positive_ref());
  2626→                                                if (loc.valid()) geom.push_back({loc.lat(), loc.lon()});
  2627→                                            } catch (...) {}
  2628→                                        }
  2629→                                        if (!geom.empty()) {
  2630→                                            int64_t first_nid = wnodes.front().positive_ref();
  2631→                                            int64_t last_nid = wnodes.back().positive_ref();
  2632→                                            local.way_geoms.push_back({way.id(), std::move(geom), first_nid, last_nid});
  2633→                                        }
  2634→                                    }
  2635→
  2636→                                    // Handle closed ways that are admin boundaries themselves
  2637→                                    // Osmium creates areas from closed ways independently of
  2638→                                    // multipolygon relations, even if the way is a relation member.
  2639→                                    if (parallel_admin) {
  2640→                                        const char* boundary = way.tags()["boundary"];
  2641→                                        if (boundary) {
  2642→                                            bool is_admin = (std::strcmp(boundary, "administrative") == 0);
  2643→                                            bool is_postal = (std::strcmp(boundary, "postal_code") == 0);
  2644→                                            if ((is_admin || is_postal) && wnodes.size() >= 4 &&
  2645→                                                wnodes.front().ref() == wnodes.back().ref()) {
  2646→                                                // Closed way forming an admin polygon
  2647→                                                uint8_t al = 0;
  2648→                                                if (is_admin) {
  2649→                                                    const char* level_str = way.tags()["admin_level"];
  2650→                                                    if (level_str) al = static_cast<uint8_t>(std::atoi(level_str));
  2651→                                                } else {
  2652→                                                    al = 11;
  2653→                                                }
  2654→                                                int max_al = is_postal ? 11 : 10;
  2655→                                                if (al >= 2 && al <= max_al && (kMaxAdminLevel == 0 || al <= kMaxAdminLevel)) {
  2656→                                                    const char* aname = way.tags()["name"];
  2657→                                                    if (!aname && is_admin) continue; // admin needs name
  2658→                                                    std::string name_str;
  2659→                                                    if (is_postal) {
  2660→                                                        const char* pc = way.tags()["postal_code"];
  2661→                                                        if (!pc) pc = aname;
  2662→                                                        if (!pc) continue; // postal needs postal_code or name
  2663→                                                        name_str = pc;
  2664→                                                    } else {
  2665→                                                        name_str = aname;
  2666→                                                    }
  2667→                                                    {
  2668→                                                        std::vector<std::pair<double,double>> verts;
  2669→                                                        bool all_valid = true;
  2670→                                                        for (const auto& nr : wnodes) {
  2671→                                                            try {
  2672→                                                                auto loc = index.get(nr.positive_ref());
  2673→                                                                if (loc.valid()) {
  2674→                                                                    verts.push_back({loc.lat(), loc.lon()});
  2675→                                                                } else { all_valid = false; break; }
  2676→                                                            } catch (...) { all_valid = false; break; }
  2677→                                                        }
  2678→                                                        if (all_valid && verts.size() >= 3) {
  2679→                                                            std::string cc;
  2680→                                                            if (al == 2) {
  2681→                                                                const char* iso = way.tags()["ISO3166-1:alpha2"];
  2682→                                                                if (iso) cc = iso;
  2683→                                                            }
  2684→                                                            local.closed_way_admins.push_back({
  2685→                                                                std::move(verts),
  2686→                                                                std::move(name_str),
  2687→                                                                al, std::move(cc)
  2688→                                                            });
  2689→                                                        }
  2690→                                                    }
  2691→                                                }
  2692→                                            }
  2693→                                        }
  2694→                                    }
  2695→                                }
  2696→                            }
  2697→
  2698→                            uint64_t done = way_blocks_done.fetch_add(1) + 1;
  2699→                            if (done % 1000 == 0) {
  2700→                                std::cerr << "  Processed " << done << " way blocks..." << std::endl;
  2701→                            }
  2702→                        }
  2703→                    });
  2704→                }
  2705→
  2706→                // Way reader: use the combined_reader (continuing from where nodes left off)
  2707→                {
  2708→                    // First, push the stashed way buffer
  2709→                    if (first_way_buf) {
  2710→                        std::unique_lock<std::mutex> lock(way_queue_mutex);
  2711→                        way_queue_cv.wait(lock, [&]{ return way_block_queue.size() < WAY_MAX_QUEUE; });
  2712→                        way_block_queue.push_back(std::move(first_way_buf));
  2713→                        way_queue_cv.notify_one();
  2714→                    }
  2715→                    // Continue reading remaining blocks from the same reader
  2716→                    while (auto buf = combined_reader.read()) {
  2717→                        {
  2718→                            std::unique_lock<std::mutex> lock(way_queue_mutex);
  2719→                            way_queue_cv.wait(lock, [&]{ return way_block_queue.size() < WAY_MAX_QUEUE; });
  2720→                            way_block_queue.push_back(std::move(buf));
  2721→                        }
  2722→                        way_queue_cv.notify_one();
  2723→                    }
  2724→                    combined_reader.close();
  2725→                    {
  2726→                        std::lock_guard<std::mutex> lock(way_queue_mutex);
  2727→                        way_reader_done = true;
  2728→                    }
  2729→                    way_queue_cv.notify_all();
  2730→                }
  2731→
  2732→                for (auto& w : workers) w.join();
  2733→                std::cerr << "  Parallel way processing complete." << std::endl;
  2734→
  2735→                // Process areas/multipolygons — sequential fallback path
  2736→                if (!parallel_admin) {
  2737→                    // Use a custom location handler that reads from our dense index
  2738→                    std::cerr << "  Processing multipolygon areas (sequential)..." << std::endl;
  2739→                    {
  2740→                        struct DenseLocationHandler : public osmium::handler::Handler {
  2741→                            DenseIndex& idx;
  2742→                            explicit DenseLocationHandler(DenseIndex& i) : idx(i) {}
  2743→                            void node(osmium::Node& node) {
  2744→                                node.set_location(idx.get(node.positive_id()));
  2745→                            }
  2746→                            void way(osmium::Way& way) {
  2747→                                for (auto& nr : way.nodes()) {
  2748→                                    nr.set_location(idx.get(nr.positive_ref()));
  2749→                                }
  2750→                            }
  2751→                        } dense_loc_handler{index};
  2752→
  2753→                        osmium::io::Reader reader_rels{input_file};
  2754→                        osmium::apply(reader_rels, dense_loc_handler,
  2755→                            mp_manager.handler([&handler](osmium::memory::Buffer&& buffer) {
  2756→                                osmium::apply(buffer, handler);
  2757→                            }));
  2758→                        reader_rels.close();
  2759→                    }
  2760→                }
  2761→
  2762→                // Merge thread-local way/interp data into main ParsedData
  2763→                std::cerr << "  Merging thread-local data..." << std::endl;
  2764→                uint64_t total_ways = 0, total_building_addrs = 0, total_interps = 0;
  2765→                for (auto& local : tld) {
  2766→                    uint32_t way_base = static_cast<uint32_t>(data.ways.size());
  2767→                    uint32_t node_base = static_cast<uint32_t>(data.street_nodes.size());
  2768→                    uint32_t interp_base = static_cast<uint32_t>(data.interp_ways.size());
  2769→                    uint32_t interp_node_base = static_cast<uint32_t>(data.interp_nodes.size());
  2770→
  2771→                    // Merge street ways
  2772→                    for (size_t i = 0; i < local.ways.size(); i++) {
  2773→                        auto h = local.ways[i];
  2774→                        h.node_offset += node_base;
  2775→                        h.name_id = data.string_pool.intern(local.way_strings[i]);
  2776→                        data.ways.push_back(h);
  2777→                    }
  2778→                    data.street_nodes.insert(data.street_nodes.end(),
  2779→                        local.street_nodes.begin(), local.street_nodes.end());
  2780→
  2781→                    // Remap deferred ways
  2782→                    for (auto dw : local.deferred_ways) {
  2783→                        dw.way_id += way_base;
  2784→                        dw.node_offset += node_base;
  2785→                        data.deferred_ways.push_back(dw);
  2786→                    }
  2787→
  2788→                    // Merge building addresses
  2789→                    for (size_t i = 0; i < local.building_addrs.size(); i++) {
  2790→                        uint64_t dummy = 0;
  2791→                        add_addr_point(data, local.building_addrs[i].lat, local.building_addrs[i].lng,
  2792→                                       local.addr_strings[i].first.c_str(),
  2793→                                       local.addr_strings[i].second.c_str(), dummy);
  2794→                    }
  2795→
  2796→                    // Merge interpolation ways
  2797→                    for (size_t i = 0; i < local.interp_ways.size(); i++) {
  2798→                        auto iw = local.interp_ways[i];
  2799→                        iw.node_offset += interp_node_base;
  2800→                        iw.street_id = data.string_pool.intern(local.interp_strings[i]);
  2801→                        data.interp_ways.push_back(iw);
  2802→                    }
  2803→                    data.interp_nodes.insert(data.interp_nodes.end(),
  2804→                        local.interp_nodes.begin(), local.interp_nodes.end());
  2805→
  2806→                    // Remap deferred interps
  2807→                    for (auto di : local.deferred_interps) {
  2808→                        di.interp_id += interp_base;
  2809→                        di.node_offset += interp_node_base;
  2810→                        data.deferred_interps.push_back(di);
  2811→                    }
  2812→
  2813→                    total_ways += local.way_count;
  2814→                    total_building_addrs += local.building_addr_count;
  2815→                    total_interps += local.interp_count;
  2816→
  2817→                    // Merge way geometries for parallel admin assembly
  2818→                    if (parallel_admin) {
  2819→                        for (auto& wg : local.way_geoms) {
  2820→                            ParsedData::WayGeometry g;
  2821→                            g.coords = std::move(wg.coords);
  2822→                            g.first_node_id = wg.first_node_id;
  2823→                            g.last_node_id = wg.last_node_id;
  2824→                            data.way_geometries[wg.way_id] = std::move(g);
  2825→                        }
  2826→                        local.way_geoms.clear();
  2827→                        local.way_geoms.shrink_to_fit();
  2828→                    }
  2829→                }
  2830→                std::cerr << "  Merged: " << total_ways << " ways, "
  2831→                          << total_building_addrs << " building addrs, "
  2832→                          << total_interps << " interps from parallel processing." << std::endl;
  2833→
  2834→                // --- Merge closed-way admin polygons ---
  2835→                if (parallel_admin) {
  2836→                    uint64_t closed_way_admin_count = 0;
  2837→                    for (auto& local : tld) {
  2838→                        for (auto& cwa : local.closed_way_admins) {
  2839→                            const char* cc = cwa.country_code.empty() ? nullptr : cwa.country_code.c_str();
  2840→                            add_admin_polygon(data, cwa.vertices, cwa.name.c_str(),
  2841→                                              cwa.admin_level, cc, &admin_pool);
  2842→                            closed_way_admin_count++;
  2843→                        }
  2844→                        local.closed_way_admins.clear();
  2845→                    }
  2846→                    if (closed_way_admin_count > 0) {
  2847→                        std::cerr << "  Added " << closed_way_admin_count
  2848→                                  << " admin polygons from closed ways." << std::endl;
  2849→                    }
  2850→                }
  2851→
  2852→                // --- Parallel admin boundary assembly ---
  2853→                if (parallel_admin) {
  2854→                    log_phase("Pass 2b: way processing", _pt);
  2855→                    std::cerr << "  Assembling admin polygons in parallel ("
  2856→                              << data.collected_relations.size() << " relations, "
  2857→                              << data.way_geometries.size() << " way geometries)..." << std::endl;
  2858→
  2859→                    // Thread-local admin results (to avoid locking add_admin_polygon)
  2860→                    struct AdminResult {
  2861→                        std::vector<std::pair<double,double>> vertices;
  2862→                        std::string name;
  2863→                        uint8_t admin_level;
  2864→                        std::string country_code;
  2865→                    };
  2866→
  2867→                    std::vector<std::vector<AdminResult>> thread_admin_results(num_threads);
  2868→                    std::atomic<size_t> rel_idx{0};
  2869→                    std::atomic<uint64_t> assembled_count{0};
  2870→
  2871→                    std::vector<std::thread> admin_workers;
  2872→                    for (unsigned int t = 0; t < num_threads; t++) {
  2873→                        admin_workers.emplace_back([&, t]() {
  2874→                            auto& local_results = thread_admin_results[t];
  2875→                            while (true) {
  2876→                                size_t i = rel_idx.fetch_add(1);
  2877→                                if (i >= data.collected_relations.size()) break;
  2878→
  2879→                                const auto& rel = data.collected_relations[i];
  2880→
  2881→                                // Skip level 2 border-line relations — these have names like
  2882→                                // "Deutschland - Österreich" (two countries separated by
  2883→                                // " - " or " — "). They are boundary lines, not country
  2884→                                // polygons, and produce self-intersecting geometry.
  2885→                                if (rel.admin_level == 2 && rel.country_code.empty() &&
  2886→                                    (rel.name.find(" - ") != std::string::npos ||
  2887→                                     rel.name.find(" \xe2\x80\x94 ") != std::string::npos)) { // " — " (em dash)
  2888→                                    assembled_count.fetch_add(1);
  2889→                                    continue;
  2890→                                }
  2891→
  2892→                                // Skip relations with missing member ways —
  2893→                                // these cross the extract boundary and would produce
  2894→                                // incorrect partial polygons
  2895→                                bool has_missing = false;
  2896→                                for (const auto& [way_id, role] : rel.members) {
  2897→                                    if (data.way_geometries.find(way_id) == data.way_geometries.end()) {
  2898→                                        has_missing = true;
  2899→                                        break;
  2900→                                    }
  2901→                                }
  2902→                                if (has_missing) {
  2903→                                    assembled_count.fetch_add(1);
  2904→                                    continue;
  2905→                                }
  2906→
  2907→                                auto rings = assemble_outer_rings(rel.members, data.way_geometries);
  2908→                                // If outer-only assembly failed, retry with all ways.
  2909→                                // Osmium ignores roles during assembly (check_roles=false) —
  2910→                                // inner ways sometimes form the boundary or bridge gaps.
  2911→                                // Filter retry results: discard rings with duplicate coords
  2912→                                // (figure-8 shapes from merging holes with outer boundary).
  2913→                                if (rings.empty()) {
  2914→                                    auto retry = assemble_outer_rings(rel.members, data.way_geometries, true);
  2915→                                    auto ck = [](double lat, double lng) -> int64_t {
  2916→                                        int32_t ilat = static_cast<int32_t>(lat * 1e7 + (lat >= 0 ? 0.5 : -0.5));
  2917→                                        int32_t ilng = static_cast<int32_t>(lng * 1e7 + (lng >= 0 ? 0.5 : -0.5));
  2918→                                        return (static_cast<int64_t>(ilat) << 32) | static_cast<uint32_t>(ilng);
  2919→                                    };
  2920→                                    for (auto& ring : retry) {
  2921→                                        if (!ring_has_duplicate_coords(ring, ck)) {
  2922→                                            rings.push_back(std::move(ring));
  2923→                                        }
  2924→                                    }
  2925→                                }
  2926→
  2927→                                // Diagnostic: log relations with 0 rings produced
  2928→                                if (rings.empty() && !rel.members.empty()) {
  2929→                                    int total_ways = 0, found = 0, missing = 0;
  2930→                                    for (const auto& [way_id, role] : rel.members) {
  2931→                                        total_ways++;
  2932→                                        auto it = data.way_geometries.find(way_id);
  2933→                                        if (it != data.way_geometries.end() && !it->second.coords.empty()) {
  2934→                                            found++;
  2935→                                        } else {
  2936→                                            missing++;
  2937→                                        }
  2938→                                    }
  2939→                                    if (missing == 0 && found > 0) {
  2940→                                        std::string detail = "  DEBUG '" + rel.name + "': ways:";
  2941→                                        for (const auto& [way_id, role] : rel.members) {
  2942→                                            auto it = data.way_geometries.find(way_id);
  2943→                                            if (it == data.way_geometries.end()) continue;
  2944→                                            detail += " [w" + std::to_string(way_id) +
  2945→                                                      " first=" + std::to_string(it->second.first_node_id) +
  2946→                                                      " last=" + std::to_string(it->second.last_node_id) +
  2947→                                                      " n=" + std::to_string(it->second.coords.size()) + "]";
  2948→                                        }
  2949→                                        std::cerr << detail << std::endl;
  2950→                                    }
  2951→                                    std::cerr << "  WARN: relation '" << rel.name
  2952→                                              << "' (level " << (int)rel.admin_level
  2953→                                              << ", " << rel.members.size() << " members): "
  2954→                                              << "0 rings from " << total_ways << " ways ("
  2955→                                              << found << " found, " << missing << " missing)" << std::endl;
  2956→                                }
  2957→
  2958→                                for (auto& ring : rings) {
  2959→                                    if (ring.size() >= 3) {
  2960→                                        AdminResult ar;
  2961→                                        ar.vertices = std::move(ring);
  2962→                                        ar.name = rel.name;
  2963→                                        ar.admin_level = rel.admin_level;
  2964→                                        ar.country_code = rel.country_code;
  2965→                                        local_results.push_back(std::move(ar));
  2966→                                    }
  2967→                                }
  2968→
  2969→                                uint64_t done = assembled_count.fetch_add(1) + 1;
  2970→                                if (done % 10000 == 0) {
  2971→                                    std::cerr << "  Assembled " << done / 1000
  2972→                                              << "K admin relations..." << std::endl;
  2973→                                }
  2974→                            }
  2975→                        });
  2976→                    }
  2977→                    for (auto& w : admin_workers) w.join();
  2978→
  2979→                    // Merge thread-local admin results into ParsedData (sequential, thread-safe)
  2980→                    uint64_t total_admin_rings = 0;
  2981→                    for (auto& local_results : thread_admin_results) {
  2982→                        for (auto& ar : local_results) {
  2983→                            const char* cc = ar.country_code.empty() ? nullptr : ar.country_code.c_str();
  2984→                            add_admin_polygon(data, ar.vertices, ar.name.c_str(),
  2985→                                              ar.admin_level, cc, &admin_pool);
  2986→                            total_admin_rings++;
  2987→                        }
  2988→                    }
  2989→                    std::cerr << "  Parallel admin assembly complete: "
  2990→                              << total_admin_rings << " polygon rings from "
  2991→                              << data.collected_relations.size() << " relations." << std::endl;
  2992→
  2993→                    // Free collected data
  2994→                    data.collected_relations.clear();
  2995→                    data.collected_relations.shrink_to_fit();
  2996→                    data.way_geometries.clear();
  2997→                    std::unordered_map<int64_t, ParsedData::WayGeometry>().swap(data.way_geometries);
  2998→                }
  2999→            }
  3000→        }
  3001→
  3002→        std::cerr << "Done reading:" << std::endl;
  3003→        std::cerr << "  " << data.ways.size() << " street ways" << std::endl;
  3004→        std::cerr << "  " << data.addr_points.size() << " address points" << std::endl;
  3005→        std::cerr << "  " << data.interp_ways.size() << " interpolation ways" << std::endl;
  3006→        std::cerr << "  " << data.admin_polygons.size() << " admin polygon rings" << std::endl;
  3007→
  3008→        // Drain admin polygon thread pool (may still be processing)
  3009→        std::cerr << "Waiting for admin polygon S2 covering to complete..." << std::endl;
  3010→        auto admin_results = admin_pool.drain();
  3011→        for (auto& [cell_id, ids] : admin_results) {
  3012→            auto& target = data.cell_to_admin[cell_id];
  3013→            target.insert(target.end(), ids.begin(), ids.end());
  3014→        }
  3015→        std::cerr << "Admin polygon S2 covering complete (" << data.cell_to_admin.size() << " cells)." << std::endl;
  3016→
  3017→        // --- Parallel S2 cell computation for ways and interpolation ---
  3018→        {
  3019→            log_phase("Admin assembly", _pt);
  3020→            std::cerr << "Computing S2 cells for ways with " << num_threads << " threads..." << std::endl;
  3021→
  3022→            // Flat-array approach: threads emit (cell_id, item_id) pairs into
  3023→            // thread-local vectors, then we sort globally and group by cell_id.
  3024→            // This avoids hash maps entirely — sort is cache-friendly O(n log n).
  3025→
  3026→            struct CellItem { uint64_t cell_id; uint32_t item_id; };
  3027→
  3028→            auto _s2t = std::chrono::steady_clock::now();
  3029→
  3030→            // Process streets: emit (cell_id, way_id) pairs
  3031→            std::cerr << "  Processing " << data.deferred_ways.size() << " street ways..." << std::endl;
  3032→            std::vector<std::vector<CellItem>> way_pairs(num_threads);
  3033→            {
  3034→                std::atomic<size_t> way_idx{0};
  3035→                std::vector<std::thread> threads;
  3036→                for (unsigned int t = 0; t < num_threads; t++) {
  3037→                    threads.emplace_back([&, t]() {
  3038→                        auto& local = way_pairs[t];
  3039→                        local.reserve(data.deferred_ways.size() / num_threads * 3);
  3040→                        std::vector<S2CellId> edge_cells;
  3041→                        std::vector<uint64_t> way_cells;
  3042→                        while (true) {
  3043→                            size_t i = way_idx.fetch_add(1);
  3044→                            if (i >= data.deferred_ways.size()) break;
  3045→                            const auto& dw = data.deferred_ways[i];
  3046→                            way_cells.clear();
  3047→                            for (uint8_t j = 0; j + 1 < dw.node_count; j++) {
  3048→                                const auto& n1 = data.street_nodes[dw.node_offset + j];
  3049→                                const auto& n2 = data.street_nodes[dw.node_offset + j + 1];
  3050→                                cover_edge(n1.lat, n1.lng, n2.lat, n2.lng, edge_cells);
  3051→                                for (const auto& c : edge_cells) way_cells.push_back(c.id());
  3052→                            }
  3053→                            std::sort(way_cells.begin(), way_cells.end());
  3054→                            way_cells.erase(std::unique(way_cells.begin(), way_cells.end()), way_cells.end());
  3055→                            for (uint64_t cell_id : way_cells) {
  3056→                                local.push_back({cell_id, dw.way_id});
  3057→                            }
  3058→                        }
  3059→                    });
  3060→                }
  3061→                for (auto& t : threads) t.join();
  3062→            }
  3063→            log_phase("  S2: street ways (parallel)", _s2t);
  3064→
  3065→            // Process interpolations: emit (cell_id, interp_id) pairs
  3066→            std::cerr << "  Processing " << data.deferred_interps.size() << " interpolation ways..." << std::endl;
  3067→            std::vector<std::vector<CellItem>> interp_pairs(num_threads);
  3068→            {
  3069→                std::atomic<size_t> interp_idx{0};
  3070→                std::vector<std::thread> threads;
  3071→                for (unsigned int t = 0; t < num_threads; t++) {
  3072→                    threads.emplace_back([&, t]() {
  3073→                        auto& local = interp_pairs[t];
  3074→                        std::vector<S2CellId> edge_cells;
  3075→                        std::vector<uint64_t> way_cells;
  3076→                        while (true) {
  3077→                            size_t i = interp_idx.fetch_add(1);
  3078→                            if (i >= data.deferred_interps.size()) break;
  3079→                            const auto& di = data.deferred_interps[i];
  3080→                            way_cells.clear();
  3081→                            for (uint8_t j = 0; j + 1 < di.node_count; j++) {
  3082→                                const auto& n1 = data.interp_nodes[di.node_offset + j];
  3083→                                const auto& n2 = data.interp_nodes[di.node_offset + j + 1];
  3084→                                cover_edge(n1.lat, n1.lng, n2.lat, n2.lng, edge_cells);
  3085→                                for (const auto& c : edge_cells) way_cells.push_back(c.id());
  3086→                            }
  3087→                            std::sort(way_cells.begin(), way_cells.end());
  3088→                            way_cells.erase(std::unique(way_cells.begin(), way_cells.end()), way_cells.end());
  3089→                            for (uint64_t cell_id : way_cells) {
  3090→                                local.push_back({cell_id, di.interp_id});
  3091→                            }
  3092→                        }
  3093→                    });
  3094→                }