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