   300→                        cr.country_code = std::move(country_code);
   301→                        cr.is_postal = is_postal;
   302→
   303→                        for (size_t mi = 0; mi < rel.members.size(); mi++) {
   304→                            if (rel.members[mi].type == 'w') {
   305→                                cr.members.emplace_back(rel.members[mi].ref, rel.member_role(mi));
   306→                            }
   307→                        }
   308→
   309→                        if (!cr.members.empty()) {
   310→                            std::lock_guard<std::mutex> lock(rel_mutex);
   311→                            data.collected_relations.push_back(std::move(cr));
   312→                        }
   313→                    }
   314→                }, "r");
   315→            }
   316→            std::cerr << "  Collected " << data.collected_relations.size()
   317→                      << " admin/postal relations for parallel assembly." << std::endl;
   318→            log_phase("Pass 1: relation scanning", _pt, _cpu);
   319→
   320→            // Build admin_way_ids as a bitset for O(1) lookup
   321→            // Max way ID ~1.3B → ~162 MiB bitset (much faster than unordered_set)
   322→            static constexpr size_t MAX_WAY_ID = 2000000000ULL; // 2B, ~250 MiB
   323→            std::vector<uint8_t> admin_way_bits(MAX_WAY_ID / 8 + 1, 0);
   324→            size_t admin_way_count = 0;
   325→            for (const auto& rel : data.collected_relations) {
   326→                for (const auto& [way_id, role] : rel.members) {
   327→                    if (way_id > 0 && static_cast<size_t>(way_id) < MAX_WAY_ID) {
   328→                        admin_way_bits[way_id / 8] |= (1 << (way_id % 8));
   329→                        admin_way_count++;
   330→                    }
   331→                }
   332→            }
   333→            auto is_admin_way = [&](int64_t id) -> bool {
   334→                if (id <= 0 || static_cast<size_t>(id) >= MAX_WAY_ID) return false;
   335→                return admin_way_bits[id / 8] & (1 << (id % 8));
   336→            };
   337→            std::cerr << "  Admin assembly needs " << admin_way_count << " way geometries." << std::endl;
   338→
   339→            // --- Pass 2: Node processing (streaming — no PbfNode objects) ---
   340→            std::cerr << "  Pass 2: processing nodes with " << num_threads << " threads..." << std::endl;
   341→            {
   342→                struct NodeThreadLocal {
   343→                    std::vector<std::pair<double,double>> addr_coords;
   344→                    std::vector<std::pair<std::string,std::string>> addr_strings;
   345→                    uint64_t count = 0;
   346→                };
   347→                // Use thread_local for streaming callback (no thread index available)
   348→                static thread_local NodeThreadLocal* tl_node_data = nullptr;
   349→                std::vector<NodeThreadLocal> ntld(num_threads);
   350→                std::atomic<unsigned> next_tl{0};
   351→
   352→                pbf.read_nodes_streaming([&](int64_t id, double lat, double lng,
   353→                    const uint32_t* tag_keys, const uint32_t* tag_vals, size_t ntags,
   354→                    const std::vector<std::string>& st) {
   355→
   356→                    // Lazy init thread-local pointer
   357→                    if (!tl_node_data) {
   358→                        unsigned idx = next_tl.fetch_add(1);
   359→                        tl_node_data = &ntld[idx % ntld.size()];
   360→                    }
   361→
   362→                    if (id > 0) {
   363→                        index.set(static_cast<uint64_t>(id), lat, lng);
   364→                    }
   365→
   366→                    // Check for address tags (fast — no string copies for tagless nodes)
   367→                    if (ntags > 0) {
   368→                        const char* housenumber = nullptr;
   369→                        const char* street = nullptr;
   370→                        for (size_t i = 0; i < ntags; i++) {
   371→                            if (tag_keys[i] < st.size()) {
   372→                                if (st[tag_keys[i]] == "addr:housenumber")
   373→                                    housenumber = tag_vals[i] < st.size() ? st[tag_vals[i]].c_str() : nullptr;
   374→                                else if (st[tag_keys[i]] == "addr:street")
   375→                                    street = tag_vals[i] < st.size() ? st[tag_vals[i]].c_str() : nullptr;
   376→                            }
   377→                        }
   378→                        if (housenumber && street) {
   379→                            tl_node_data->addr_coords.push_back({lat, lng});
   380→                            tl_node_data->addr_strings.push_back({housenumber, street});
   381→                            tl_node_data->count++;
   382→                        }
   383→                    }
   384→                });
   385→
   386→                // Reset thread_local for next use
   387→                tl_node_data = nullptr;
   388→
   389→                // Merge address points
   390→                uint64_t total_addrs = 0;
   391→                for (auto& local : ntld) {
   392→                    for (size_t j = 0; j < local.addr_coords.size(); j++) {
   393→                        uint64_t dummy = 0;
   394→                        add_addr_point(data, local.addr_coords[j].first, local.addr_coords[j].second,
   395→                                       local.addr_strings[j].first.c_str(),
   396→                                       local.addr_strings[j].second.c_str(), dummy);
   397→                    }
   398→                    total_addrs += local.count;
   399→                }
   400→                std::cerr << "  Node processing complete: " << total_addrs
   401→                          << " address points collected." << std::endl;
   402→            }
   403→            log_phase("Pass 2: node processing", _pt, _cpu);
   404→            pbf.release_pages(); // free PBF mmap pages, will re-fault for way pass
   405→
   406→            // --- Pass 2b: Way processing (fully parallel) ---
   407→            std::cerr << "  Processing ways with " << num_threads << " threads..." << std::endl;
   408→            {
   409→                // Thread-local data for parallel way processing
   410→                struct ThreadLocalData {
   411→                    std::vector<WayHeader> ways;
   412→                    std::vector<NodeCoord> street_nodes;
   413→                    std::vector<DeferredWay> deferred_ways;
   414→                    std::vector<InterpWay> interp_ways;
   415→                    std::vector<NodeCoord> interp_nodes;
   416→                    std::vector<DeferredInterp> deferred_interps;
   417→                    std::vector<AddrPoint> building_addrs;
   418→                    std::vector<std::pair<double, double>> building_addr_coords; // lat,lng for S2 cell
   419→                    std::vector<std::string> way_strings;      // way name strings
   420→                    std::vector<std::pair<std::string,std::string>> addr_strings; // building addr {hn, street}
   421→                    std::vector<std::string> interp_strings;   // interp street name strings
   422→                    uint64_t way_count = 0;
   423→                    uint64_t building_addr_count = 0;
   424→                    uint64_t interp_count = 0;
   425→                    // Way geometries for parallel admin assembly
   426→                    struct WayGeomEntry {
   427→                        int64_t way_id;
   428→                        std::vector<std::pair<double,double>> coords;
   429→                        int64_t first_node_id;
   430→                        int64_t last_node_id;
   431→                    };
   432→                    std::vector<WayGeomEntry> way_geoms;
   433→                    // Closed-way admin polygons (ways with boundary=administrative)
   434→                    struct ClosedWayAdmin {
   435→                        std::vector<std::pair<double,double>> vertices;
   436→                        std::string name;
   437→                        uint8_t admin_level;
   438→                        std::string country_code;
   439→                    };
   440→                    std::vector<ClosedWayAdmin> closed_way_admins;
   441→                };
   442→
   443→                static thread_local ThreadLocalData* tl_way_data = nullptr;
   444→                std::vector<ThreadLocalData> tld(num_threads);
   445→                std::atomic<unsigned> next_tl_way{0};
   446→
   447→                pbf.read_ways_streaming([&](int64_t way_id,
   448→                    const int64_t* refs_data, size_t refs_size,
   449→                    const uint32_t* tag_keys, const uint32_t* tag_vals, size_t ntags,
   450→                    const std::vector<std::string>& st) {
   451→
   452→                    if (!tl_way_data) {
   453→                        unsigned idx = next_tl_way.fetch_add(1);
   454→                        tl_way_data = &tld[idx % tld.size()];
   455→                    }
   456→                    auto& local = *tl_way_data;
   457→
   458→                    // Single-pass tag extraction — avoid repeated linear scans
   459→                    const char* t_interpolation = nullptr;
   460→                    const char* t_housenumber = nullptr;
   461→                    const char* t_street = nullptr;
   462→                    const char* t_highway = nullptr;
   463→                    const char* t_name = nullptr;
   464→                    const char* t_boundary = nullptr;
   465→                    const char* t_admin_level = nullptr;
   466→                    const char* t_postal_code = nullptr;
   467→                    const char* t_iso = nullptr;
   468→                    for (size_t i = 0; i < ntags; i++) {
   469→                        if (tag_keys[i] >= st.size()) continue;
   470→                        const auto& k = st[tag_keys[i]];
   471→                        const char* v = tag_vals[i] < st.size() ? st[tag_vals[i]].c_str() : nullptr;
   472→                        // Fast dispatch by first character to avoid 9 string comparisons
   473→                        switch (k.size() > 0 ? k[0] : 0) {
   474→                            case 'a':
   475→                                if (k == "addr:interpolation") t_interpolation = v;
   476→                                else if (k == "addr:housenumber") t_housenumber = v;
   477→                                else if (k == "addr:street") t_street = v;
   478→                                else if (k == "admin_level") t_admin_level = v;
   479→                                break;
   480→                            case 'h': if (k == "highway") t_highway = v; break;
   481→                            case 'n': if (k == "name") t_name = v; break;
   482→                            case 'b': if (k == "boundary") t_boundary = v; break;
   483→                            case 'p': if (k == "postal_code") t_postal_code = v; break;
   484→                            case 'I': if (k == "ISO3166-1:alpha2") t_iso = v; break;
   485→                        }
   486→                    }
   487→
   488→                    // Early exit: if no relevant tags, skip expensive node resolution
   489→                    bool need_nodes = t_interpolation || t_housenumber ||
   490→                        (t_highway && is_included_highway(t_highway) && t_name) ||
   491→                        t_boundary ||
   492→                        (refs_size > 0 && is_admin_way(way_id));
   493→                    if (!need_nodes) return;
   494→
   495→                    // Pre-resolve all node locations
   496→                    thread_local std::vector<PackedLocation> resolved_locs;
   497→                    resolved_locs.clear();
   498→                    resolved_locs.reserve(refs_size);
   499→                    bool all_valid = true;

                        cr.country_code = std::move(country_code);
                        cr.is_postal = is_postal;

                        for (size_t mi = 0; mi < rel.members.size(); mi++) {
                            if (rel.members[mi].type == 'w') {
                                cr.members.emplace_back(rel.members[mi].ref, rel.member_role(mi));
                            }
                        }

                        if (!cr.members.empty()) {
                            std::lock_guard<std::mutex> lock(rel_mutex);
                            data.collected_relations.push_back(std::move(cr));
                        }
                    }
                }, "r");
            }
            std::cerr << "  Collected " << data.collected_relations.size()
                      << " admin/postal relations for parallel assembly." << std::endl;
            log_phase("Pass 1: relation scanning", _pt, _cpu);

            // Build admin_way_ids as a bitset for O(1) lookup
            // Max way ID ~1.3B → ~162 MiB bitset (much faster than unordered_set)
            static constexpr size_t MAX_WAY_ID = 2000000000ULL; // 2B, ~250 MiB
            std::vector<uint8_t> admin_way_bits(MAX_WAY_ID / 8 + 1, 0);
            size_t admin_way_count = 0;
            for (const auto& rel : data.collected_relations) {
                for (const auto& [way_id, role] : rel.members) {
                    if (way_id > 0 && static_cast<size_t>(way_id) < MAX_WAY_ID) {
                        admin_way_bits[way_id / 8] |= (1 << (way_id % 8));
                        admin_way_count++;
                    }
                }
            }
            auto is_admin_way = [&](int64_t id) -> bool {
                if (id <= 0 || static_cast<size_t>(id) >= MAX_WAY_ID) return false;
                return admin_way_bits[id / 8] & (1 << (id % 8));
            };
            std::cerr << "  Admin assembly needs " << admin_way_count << " way geometries." << std::endl;

            // --- Pass 2: Node processing (streaming — no PbfNode objects) ---
            std::cerr << "  Pass 2: processing nodes with " << num_threads << " threads..." << std::endl;
            {
                struct NodeThreadLocal {
                    std::vector<std::pair<double,double>> addr_coords;
                    std::vector<std::pair<std::string,std::string>> addr_strings;
                    uint64_t count = 0;
                };
                // Use thread_local for streaming callback (no thread index available)
                static thread_local NodeThreadLocal* tl_node_data = nullptr;
                std::vector<NodeThreadLocal> ntld(num_threads);
                std::atomic<unsigned> next_tl{0};

                pbf.read_nodes_streaming([&](int64_t id, double lat, double lng,
                    const uint32_t* tag_keys, const uint32_t* tag_vals, size_t ntags,
                    const std::vector<std::string>& st) {

                    // Lazy init thread-local pointer
                    if (!tl_node_data) {
                        unsigned idx = next_tl.fetch_add(1);
                        tl_node_data = &ntld[idx % ntld.size()];
                    }

                    if (id > 0) {
                        index.set(static_cast<uint64_t>(id), lat, lng);
                    }

                    // Check for address tags (fast — no string copies for tagless nodes)
                    if (ntags > 0) {
                        const char* housenumber = nullptr;
                        const char* street = nullptr;
                        for (size_t i = 0; i < ntags; i++) {
                            if (tag_keys[i] < st.size()) {
                                if (st[tag_keys[i]] == "addr:housenumber")
                                    housenumber = tag_vals[i] < st.size() ? st[tag_vals[i]].c_str() : nullptr;
                                else if (st[tag_keys[i]] == "addr:street")
                                    street = tag_vals[i] < st.size() ? st[tag_vals[i]].c_str() : nullptr;
                            }
                        }
                        if (housenumber && street) {
                            tl_node_data->addr_coords.push_back({lat, lng});
                            tl_node_data->addr_strings.push_back({housenumber, street});
                            tl_node_data->count++;
                        }
                    }
                });

                // Reset thread_local for next use
                tl_node_data = nullptr;

                // 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, _cpu);
            pbf.release_pages(); // free PBF mmap pages, will re-fault for way pass

            // --- Pass 2b: Way processing (fully parallel) ---
            std::cerr << "  Processing ways with " << num_threads << " threads..." << std::endl;
            {
                // 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;
                };

                static thread_local ThreadLocalData* tl_way_data = nullptr;
                std::vector<ThreadLocalData> tld(num_threads);
                std::atomic<unsigned> next_tl_way{0};

                pbf.read_ways_streaming([&](int64_t way_id,
                    const int64_t* refs_data, size_t refs_size,
                    const uint32_t* tag_keys, const uint32_t* tag_vals, size_t ntags,
                    const std::vector<std::string>& st) {

                    if (!tl_way_data) {
                        unsigned idx = next_tl_way.fetch_add(1);
                        tl_way_data = &tld[idx % tld.size()];
                    }
                    auto& local = *tl_way_data;

                    // Single-pass tag extraction — avoid repeated linear scans
                    const char* t_interpolation = nullptr;
                    const char* t_housenumber = nullptr;
                    const char* t_street = nullptr;
                    const char* t_highway = nullptr;
                    const char* t_name = nullptr;
                    const char* t_boundary = nullptr;
                    const char* t_admin_level = nullptr;
                    const char* t_postal_code = nullptr;
                    const char* t_iso = nullptr;
                    for (size_t i = 0; i < ntags; i++) {
                        if (tag_keys[i] >= st.size()) continue;
                        const auto& k = st[tag_keys[i]];
                        const char* v = tag_vals[i] < st.size() ? st[tag_vals[i]].c_str() : nullptr;
                        // Fast dispatch by first character to avoid 9 string comparisons
                        switch (k.size() > 0 ? k[0] : 0) {
                            case 'a':
                                if (k == "addr:interpolation") t_interpolation = v;
                                else if (k == "addr:housenumber") t_housenumber = v;
                                else if (k == "addr:street") t_street = v;
                                else if (k == "admin_level") t_admin_level = v;
                                break;
                            case 'h': if (k == "highway") t_highway = v; break;
                            case 'n': if (k == "name") t_name = v; break;
                            case 'b': if (k == "boundary") t_boundary = v; break;
                            case 'p': if (k == "postal_code") t_postal_code = v; break;
                            case 'I': if (k == "ISO3166-1:alpha2") t_iso = v; break;
                        }
                    }

                    // Early exit: if no relevant tags, skip expensive node resolution
                    bool need_nodes = t_interpolation || t_housenumber ||
                        (t_highway && is_included_highway(t_highway) && t_name) ||
                        t_boundary ||
                        (refs_size > 0 && is_admin_way(way_id));
                    if (!need_nodes) return;

                    // Pre-resolve all node locations
                    thread_local std::vector<PackedLocation> resolved_locs;
                    resolved_locs.clear();
                    resolved_locs.reserve(refs_size);
                    bool all_valid = true;


   500→                    for (size_t ri = 0; ri < refs_size; ri++) {
   501→                        auto loc = index.get(static_cast<uint64_t>(refs_data[ri]));
   502→                        resolved_locs.push_back(loc);
   503→                        if (!loc.valid()) all_valid = false;
   504→                    }
   505→
   506→                    // Address interpolation
   507→                    if (t_interpolation) {
   508→                        if (refs_size >= 2 && all_valid) {
   509→                            const char* street = t_street;
   510→                            if (street) {
   511→                                uint32_t interp_id = static_cast<uint32_t>(local.interp_ways.size());
   512→                                uint32_t node_offset = static_cast<uint32_t>(local.interp_nodes.size());
   513→                                for (const auto& loc : resolved_locs)
   514→                                    local.interp_nodes.push_back({static_cast<float>(loc.lat()), static_cast<float>(loc.lon())});
   515→                                uint8_t interp_type = 0;
   516→                                if (std::strcmp(t_interpolation, "even") == 0) interp_type = 1;
   517→                                else if (std::strcmp(t_interpolation, "odd") == 0) interp_type = 2;
   518→                                InterpWay iw{}; iw.node_offset = node_offset;
   519→                                iw.node_count = static_cast<uint8_t>(std::min(refs_size, size_t(255)));
   520→                                iw.interpolation = interp_type;
   521→                                local.interp_ways.push_back(iw);
   522→                                local.interp_strings.push_back(street);
   523→                                local.deferred_interps.push_back({interp_id, node_offset, iw.node_count});
   524→                                local.interp_count++;
   525→                            }
   526→                        }
   527→                        return;
   528→                    }
   529→
   530→                    // Building addresses
   531→                    const char* housenumber = t_housenumber;
   532→                    if (housenumber) {
   533→                        const char* street = t_street;
   534→                        if (street && refs_size > 0) {
   535→                            double sum_lat = 0, sum_lng = 0; int valid = 0;
   536→                            for (const auto& loc : resolved_locs) {
   537→                                if (loc.valid()) { sum_lat += loc.lat(); sum_lng += loc.lon(); valid++; }
   538→                            }
   539→                            if (valid > 0) {
   540→                                local.building_addr_coords.push_back({sum_lat/valid, sum_lng/valid});
   541→                                local.building_addrs.push_back({static_cast<float>(sum_lat/valid), static_cast<float>(sum_lng/valid), 0, 0});
   542→                                local.addr_strings.push_back({housenumber, street});
   543→                                local.building_addr_count++;
   544→                            }
   545→                        }
   546→                    }
   547→
   548→                    // Highway ways
   549→                    if (t_highway && is_included_highway(t_highway)) {
   550→                        if (t_name && refs_size >= 2 && all_valid) {
   551→                            uint32_t wid = static_cast<uint32_t>(local.ways.size());
   552→                            uint32_t noff = static_cast<uint32_t>(local.street_nodes.size());
   553→                            for (const auto& loc : resolved_locs)
   554→                                local.street_nodes.push_back({static_cast<float>(loc.lat()), static_cast<float>(loc.lon())});
   555→                            WayHeader header{}; header.node_offset = noff;
   556→                            header.node_count = static_cast<uint8_t>(std::min(refs_size, size_t(255)));
   557→                            local.ways.push_back(header);
   558→                            local.way_strings.push_back(t_name);
   559→                            local.deferred_ways.push_back({wid, noff, header.node_count});
   560→                            local.way_count++;
   561→                        }
   562→                    }
   563→
   564→                    // Admin boundary member ways
   565→                    if (refs_size > 0 && is_admin_way(way_id)) {
   566→                        std::vector<std::pair<double,double>> geom;
   567→                        for (const auto& loc : resolved_locs)
   568→                            if (loc.valid()) geom.push_back({loc.lat(), loc.lon()});
   569→                        if (!geom.empty())
   570→                            local.way_geoms.push_back({way_id, std::move(geom), refs_data[0], refs_data[refs_size-1]});
   571→                    }
   572→
   573→                    // Closed way admin boundaries
   574→                    const char* boundary = t_boundary;
   575→                    if (boundary) {
   576→                        bool is_admin = (std::strcmp(boundary, "administrative") == 0);
   577→                        bool is_postal = (std::strcmp(boundary, "postal_code") == 0);
   578→                        if ((is_admin || is_postal) && refs_size >= 4 && refs_data[0] == refs_data[refs_size-1]) {
   579→                            uint8_t al = 0;
   580→                            if (is_admin) { const char* ls = t_admin_level; if (ls) al = static_cast<uint8_t>(std::atoi(ls)); }
   581→                            else al = 11;
   582→                            int max_al = is_postal ? 11 : 10;
   583→                            if (al >= 2 && al <= max_al && (kMaxAdminLevel == 0 || al <= kMaxAdminLevel)) {
   584→                                const char* aname = t_name;
   585→                                if (aname || !is_admin) {
   586→                                    std::string name_str;
   587→                                    if (is_postal) { const char* pc = t_postal_code; if (!pc) pc = aname; if (pc) name_str = pc; }
   588→                                    else if (aname) name_str = aname;
   589→                                    if (!name_str.empty() && all_valid && resolved_locs.size() >= 3) {
   590→                                        std::vector<std::pair<double,double>> verts;
   591→                                        for (const auto& loc : resolved_locs) verts.push_back({loc.lat(), loc.lon()});
   592→                                        std::string cc; if (al == 2) { const char* iso = t_iso; if (iso) cc = iso; }
   593→                                        local.closed_way_admins.push_back({std::move(verts), std::move(name_str), al, std::move(cc)});
   594→                                    }
   595→                                }
   596→                            }
   597→                        }
   598→                    }
   599→                });
   600→                tl_way_data = nullptr;
   601→                std::cerr << "  Parallel way processing complete." << std::endl;
   602→
   603→                // Process areas/multipolygons — sequential fallback path
   604→                // Merge thread-local way/interp data into main ParsedData
   605→                std::cerr << "  Merging thread-local data..." << std::endl;
   606→                uint64_t total_ways = 0, total_building_addrs = 0, total_interps = 0;
   607→                for (auto& local : tld) {
   608→                    uint32_t way_base = static_cast<uint32_t>(data.ways.size());
   609→                    uint32_t node_base = static_cast<uint32_t>(data.street_nodes.size());
   610→                    uint32_t interp_base = static_cast<uint32_t>(data.interp_ways.size());
   611→                    uint32_t interp_node_base = static_cast<uint32_t>(data.interp_nodes.size());
   612→
   613→                    // Merge street ways
   614→                    for (size_t i = 0; i < local.ways.size(); i++) {
   615→                        auto h = local.ways[i];
   616→                        h.node_offset += node_base;
   617→                        h.name_id = data.string_pool.intern(local.way_strings[i]);
   618→                        data.ways.push_back(h);
   619→                    }
   620→                    data.street_nodes.insert(data.street_nodes.end(),
   621→                        local.street_nodes.begin(), local.street_nodes.end());
   622→
   623→                    // Remap deferred ways
   624→                    for (auto dw : local.deferred_ways) {
   625→                        dw.way_id += way_base;
   626→                        dw.node_offset += node_base;
   627→                        data.deferred_ways.push_back(dw);
   628→                    }
   629→
   630→                    // Merge building addresses
   631→                    for (size_t i = 0; i < local.building_addrs.size(); i++) {
   632→                        uint64_t dummy = 0;
   633→                        add_addr_point(data, local.building_addrs[i].lat, local.building_addrs[i].lng,
   634→                                       local.addr_strings[i].first.c_str(),
   635→                                       local.addr_strings[i].second.c_str(), dummy);
   636→                    }
   637→
   638→                    // Merge interpolation ways
   639→                    for (size_t i = 0; i < local.interp_ways.size(); i++) {
   640→                        auto iw = local.interp_ways[i];
   641→                        iw.node_offset += interp_node_base;
   642→                        iw.street_id = data.string_pool.intern(local.interp_strings[i]);
   643→                        data.interp_ways.push_back(iw);
   644→                    }
   645→                    data.interp_nodes.insert(data.interp_nodes.end(),
   646→                        local.interp_nodes.begin(), local.interp_nodes.end());
   647→
   648→                    // Remap deferred interps
   649→                    for (auto di : local.deferred_interps) {
   650→                        di.interp_id += interp_base;
   651→                        di.node_offset += interp_node_base;
   652→                        data.deferred_interps.push_back(di);
   653→                    }
   654→
   655→                    total_ways += local.way_count;
   656→                    total_building_addrs += local.building_addr_count;
   657→                    total_interps += local.interp_count;
   658→
   659→                    // Merge way geometries for admin assembly
   660→                    {
   661→                        for (auto& wg : local.way_geoms) {
   662→                            ParsedData::WayGeometry g;
   663→                            g.coords = std::move(wg.coords);
   664→                            g.first_node_id = wg.first_node_id;
   665→                            g.last_node_id = wg.last_node_id;
   666→                            data.way_geometries[wg.way_id] = std::move(g);
   667→                        }
   668→                        local.way_geoms.clear();
   669→                        local.way_geoms.shrink_to_fit();
   670→                    }
   671→                }
   672→                std::cerr << "  Merged: " << total_ways << " ways, "
   673→                          << total_building_addrs << " building addrs, "
   674→                          << total_interps << " interps from parallel processing." << std::endl;
   675→
   676→                // --- Merge closed-way admin polygons ---
   677→                {
   678→                    uint64_t closed_way_admin_count = 0;
   679→                    for (auto& local : tld) {
   680→                        for (auto& cwa : local.closed_way_admins) {
   681→                            const char* cc = cwa.country_code.empty() ? nullptr : cwa.country_code.c_str();
   682→                            add_admin_polygon(data, cwa.vertices, cwa.name.c_str(),
   683→                                              cwa.admin_level, cc, &admin_pool);
   684→                            closed_way_admin_count++;
   685→                        }
   686→                        local.closed_way_admins.clear();
   687→                    }
   688→                    if (closed_way_admin_count > 0) {
   689→                        std::cerr << "  Added " << closed_way_admin_count
   690→                                  << " admin polygons from closed ways." << std::endl;
   691→                    }
   692→                }
   693→
   694→                // --- Parallel admin boundary assembly ---
   695→                {
   696→                    log_phase("Pass 2b: way processing", _pt, _cpu);
   697→                    pbf.unmap(); // release 86 GiB PBF mmap before admin/S2 phases
   698→                    std::cerr << "  Assembling admin polygons in parallel ("
   699→                              << data.collected_relations.size() << " relations, "

                    for (size_t ri = 0; ri < refs_size; ri++) {
                        auto loc = index.get(static_cast<uint64_t>(refs_data[ri]));
                        resolved_locs.push_back(loc);
                        if (!loc.valid()) all_valid = false;
                    }

                    // Address interpolation
                    if (t_interpolation) {
                        if (refs_size >= 2 && all_valid) {
                            const char* street = t_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& loc : resolved_locs)
                                    local.interp_nodes.push_back({static_cast<float>(loc.lat()), static_cast<float>(loc.lon())});
                                uint8_t interp_type = 0;
                                if (std::strcmp(t_interpolation, "even") == 0) interp_type = 1;
                                else if (std::strcmp(t_interpolation, "odd") == 0) interp_type = 2;
                                InterpWay iw{}; iw.node_offset = node_offset;
                                iw.node_count = static_cast<uint8_t>(std::min(refs_size, size_t(255)));
                                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++;
                            }
                        }
                        return;
                    }

                    // Building addresses
                    const char* housenumber = t_housenumber;
                    if (housenumber) {
                        const char* street = t_street;
                        if (street && refs_size > 0) {
                            double sum_lat = 0, sum_lng = 0; int valid = 0;
                            for (const auto& loc : resolved_locs) {
                                if (loc.valid()) { sum_lat += loc.lat(); sum_lng += loc.lon(); valid++; }
                            }
                            if (valid > 0) {
                                local.building_addr_coords.push_back({sum_lat/valid, sum_lng/valid});
                                local.building_addrs.push_back({static_cast<float>(sum_lat/valid), static_cast<float>(sum_lng/valid), 0, 0});
                                local.addr_strings.push_back({housenumber, street});
                                local.building_addr_count++;
                            }
                        }
                    }

                    // Highway ways
                    if (t_highway && is_included_highway(t_highway)) {
                        if (t_name && refs_size >= 2 && all_valid) {
                            uint32_t wid = static_cast<uint32_t>(local.ways.size());
                            uint32_t noff = static_cast<uint32_t>(local.street_nodes.size());
                            for (const auto& loc : resolved_locs)
                                local.street_nodes.push_back({static_cast<float>(loc.lat()), static_cast<float>(loc.lon())});
                            WayHeader header{}; header.node_offset = noff;
                            header.node_count = static_cast<uint8_t>(std::min(refs_size, size_t(255)));
                            local.ways.push_back(header);
                            local.way_strings.push_back(t_name);
                            local.deferred_ways.push_back({wid, noff, header.node_count});
                            local.way_count++;
                        }
                    }

                    // Admin boundary member ways
                    if (refs_size > 0 && is_admin_way(way_id)) {
                        std::vector<std::pair<double,double>> geom;
                        for (const auto& loc : resolved_locs)
                            if (loc.valid()) geom.push_back({loc.lat(), loc.lon()});
                        if (!geom.empty())
                            local.way_geoms.push_back({way_id, std::move(geom), refs_data[0], refs_data[refs_size-1]});
                    }

                    // Closed way admin boundaries
                    const char* boundary = t_boundary;
                    if (boundary) {
                        bool is_admin = (std::strcmp(boundary, "administrative") == 0);
                        bool is_postal = (std::strcmp(boundary, "postal_code") == 0);
                        if ((is_admin || is_postal) && refs_size >= 4 && refs_data[0] == refs_data[refs_size-1]) {
                            uint8_t al = 0;
                            if (is_admin) { const char* ls = t_admin_level; if (ls) al = static_cast<uint8_t>(std::atoi(ls)); }
                            else al = 11;
                            int max_al = is_postal ? 11 : 10;
                            if (al >= 2 && al <= max_al && (kMaxAdminLevel == 0 || al <= kMaxAdminLevel)) {
                                const char* aname = t_name;
                                if (aname || !is_admin) {
                                    std::string name_str;
                                    if (is_postal) { const char* pc = t_postal_code; if (!pc) pc = aname; if (pc) name_str = pc; }
                                    else if (aname) name_str = aname;
                                    if (!name_str.empty() && all_valid && resolved_locs.size() >= 3) {
                                        std::vector<std::pair<double,double>> verts;
                                        for (const auto& loc : resolved_locs) verts.push_back({loc.lat(), loc.lon()});
                                        std::string cc; if (al == 2) { const char* iso = t_iso; if (iso) cc = iso; }
                                        local.closed_way_admins.push_back({std::move(verts), std::move(name_str), al, std::move(cc)});
                                    }
                                }
                            }
                        }
                    }
                });
                tl_way_data = nullptr;
                std::cerr << "  Parallel way processing complete." << std::endl;

                // Process areas/multipolygons — sequential fallback path
                // Merge thread-local way/interp data into main ParsedData
                std::cerr << "  Merging thread-local data..." << std::endl;
                uint64_t total_ways = 0, total_building_addrs = 0, total_interps = 0;
                for (auto& local : tld) {
                    uint32_t way_base = static_cast<uint32_t>(data.ways.size());
                    uint32_t node_base = static_cast<uint32_t>(data.street_nodes.size());
                    uint32_t interp_base = static_cast<uint32_t>(data.interp_ways.size());
                    uint32_t interp_node_base = static_cast<uint32_t>(data.interp_nodes.size());

                    // Merge street ways
                    for (size_t i = 0; i < local.ways.size(); i++) {
                        auto h = local.ways[i];
                        h.node_offset += node_base;
                        h.name_id = data.string_pool.intern(local.way_strings[i]);
                        data.ways.push_back(h);
                    }
                    data.street_nodes.insert(data.street_nodes.end(),
                        local.street_nodes.begin(), local.street_nodes.end());

                    // Remap deferred ways
                    for (auto dw : local.deferred_ways) {
                        dw.way_id += way_base;
                        dw.node_offset += node_base;
                        data.deferred_ways.push_back(dw);
                    }

                    // Merge building addresses
                    for (size_t i = 0; i < local.building_addrs.size(); i++) {
                        uint64_t dummy = 0;
                        add_addr_point(data, local.building_addrs[i].lat, local.building_addrs[i].lng,
                                       local.addr_strings[i].first.c_str(),
                                       local.addr_strings[i].second.c_str(), dummy);
                    }

                    // Merge interpolation ways
                    for (size_t i = 0; i < local.interp_ways.size(); i++) {
                        auto iw = local.interp_ways[i];
                        iw.node_offset += interp_node_base;
                        iw.street_id = data.string_pool.intern(local.interp_strings[i]);
                        data.interp_ways.push_back(iw);
                    }
                    data.interp_nodes.insert(data.interp_nodes.end(),
                        local.interp_nodes.begin(), local.interp_nodes.end());

                    // Remap deferred interps
                    for (auto di : local.deferred_interps) {
                        di.interp_id += interp_base;
                        di.node_offset += interp_node_base;
                        data.deferred_interps.push_back(di);
                    }

                    total_ways += local.way_count;
                    total_building_addrs += local.building_addr_count;
                    total_interps += local.interp_count;

                    // Merge way geometries for admin assembly
                    {
                        for (auto& wg : local.way_geoms) {
                            ParsedData::WayGeometry g;
                            g.coords = std::move(wg.coords);
                            g.first_node_id = wg.first_node_id;
                            g.last_node_id = wg.last_node_id;
                            data.way_geometries[wg.way_id] = std::move(g);
                        }
                        local.way_geoms.clear();
                        local.way_geoms.shrink_to_fit();
                    }
                }
                std::cerr << "  Merged: " << total_ways << " ways, "
                          << total_building_addrs << " building addrs, "
                          << total_interps << " interps from parallel processing." << std::endl;

                // --- Merge closed-way admin polygons ---
                {
                    uint64_t closed_way_admin_count = 0;
                    for (auto& local : tld) {
                        for (auto& cwa : local.closed_way_admins) {
                            const char* cc = cwa.country_code.empty() ? nullptr : cwa.country_code.c_str();
                            add_admin_polygon(data, cwa.vertices, cwa.name.c_str(),
                                              cwa.admin_level, cc, &admin_pool);
                            closed_way_admin_count++;
                        }
                        local.closed_way_admins.clear();
                    }
                    if (closed_way_admin_count > 0) {
                        std::cerr << "  Added " << closed_way_admin_count
                                  << " admin polygons from closed ways." << std::endl;
                    }
                }

                // --- Parallel admin boundary assembly ---
                {
                    log_phase("Pass 2b: way processing", _pt, _cpu);
                    pbf.unmap(); // release 86 GiB PBF mmap before admin/S2 phases
                    std::cerr << "  Assembling admin polygons in parallel ("
                              << data.collected_relations.size() << " relations, "


Now let me see the S2 cell assignment and the write pipeline.


     1→#pragma once
     2→
     3→#include <condition_variable>
     4→#include <cstdint>
     5→#include <deque>
     6→#include <mutex>
     7→#include <thread>
     8→#include <unordered_map>
     9→#include <unordered_set>
    10→#include <vector>
    11→
    12→#include <s2/s2cell_id.h>
    13→
    14→#include "types.h"
    15→#include "geometry.h"
    16→#include "parsed_data.h"
    17→
    18→// --- S2 cell level globals (set by main arg parsing) ---
    19→
    20→extern int kStreetCellLevel;
    21→extern int kAdminCellLevel;
    22→extern int kMaxAdminLevel;
    23→
    24→// --- Simplification globals ---
    25→
    26→extern SimplifyMode kSimplifyMode;
    27→extern double kSimplifyEpsilonOverride; // 0 = use per-level defaults
    28→
    29→// --- S2 helpers ---
    30→
    31→void cover_edge(double lat1, double lng1, double lat2, double lng2,
    32→                std::vector<S2CellId>& out);
    33→
    34→std::vector<S2CellId> cover_edge(double lat1, double lng1, double lat2, double lng2);
    35→
    36→S2CellId point_to_cell(double lat, double lng);
    37→
    38→std::vector<std::pair<S2CellId, bool>> cover_polygon(
    39→    const std::vector<std::pair<double,double>>& vertices);
    40→
    41→// --- Thread pool for concurrent admin polygon S2 covering ---
    42→
    43→class AdminCoverPool {
    44→public:
    45→    struct WorkItem {
    46→        uint32_t poly_id;
    47→        std::vector<std::pair<double,double>> vertices;
    48→    };
    49→
    50→    explicit AdminCoverPool(size_t num_threads);
    51→    ~AdminCoverPool();
    52→
    53→    void submit(uint32_t poly_id, std::vector<std::pair<double,double>>&& vertices);
    54→    std::unordered_map<uint64_t, std::vector<uint32_t>> drain();
    55→
    56→private:
    57→    void worker_loop();
    58→
    59→    std::vector<std::thread> workers_;
    60→    std::deque<WorkItem> queue_;
    61→    std::vector<std::unordered_map<uint64_t, std::vector<uint32_t>>> thread_results_;
    62→    std::mutex mutex_;
    63→    std::condition_variable cv_;
    64→    std::condition_variable done_cv_;
    65→    size_t active_workers_ = 0;
    66→    bool stop_;
    67→};
    68→
    69→// --- Simplification dispatch ---
    70→
    71→// Simplify a polygon using the current global mode, taking admin_level into account.
    72→// In MaxVertices mode: caps at MAX_POLYGON_VERTICES (500).
    73→// In ErrorBounded mode: uses per-level epsilon in meters, converted to degrees.
    74→inline std::vector<std::pair<double,double>> simplify_admin_polygon(
    75→    const std::vector<std::pair<double,double>>& pts, uint8_t admin_level) {
    76→    if (kSimplifyMode == SimplifyMode::ErrorBounded) {
    77→        double eps_m = kSimplifyEpsilonOverride > 0
    78→            ? kSimplifyEpsilonOverride
    79→            : admin_epsilon_meters(admin_level);
    80→        // Estimate latitude from first vertex for degree conversion
    81→        double lat = pts.empty() ? 0.0 : pts[0].first;
    82→        double eps_deg = meters_to_degrees(eps_m, lat);
    83→        return simplify_polygon_epsilon(pts, eps_deg);
    84→    }
    85→    return simplify_polygon(pts);
    86→}
    87→
    88→// --- Helper functions that depend on S2 ---
    89→
    90→void add_addr_point(ParsedData& data, double lat, double lng,
    91→                    const char* housenumber, const char* street,
    92→                    uint64_t& addr_count_total);
    93→
    94→void add_admin_polygon(ParsedData& data,
    95→                       const std::vector<std::pair<double,double>>& vertices_in,
    96→                       const char* name, uint8_t admin_level,
    97→                       const char* country_code,
    98→                       AdminCoverPool* admin_pool = nullptr);
    99→

#pragma once

#include <condition_variable>
#include <cstdint>
#include <deque>
#include <mutex>
#include <thread>
#include <unordered_map>
#include <unordered_set>
#include <vector>

#include <s2/s2cell_id.h>

#include "types.h"
#include "geometry.h"
#include "parsed_data.h"

// --- S2 cell level globals (set by main arg parsing) ---

extern int kStreetCellLevel;
extern int kAdminCellLevel;
extern int kMaxAdminLevel;

// --- Simplification globals ---

extern SimplifyMode kSimplifyMode;
extern double kSimplifyEpsilonOverride; // 0 = use per-level defaults

// --- S2 helpers ---

void cover_edge(double lat1, double lng1, double lat2, double lng2,
                std::vector<S2CellId>& out);

std::vector<S2CellId> cover_edge(double lat1, double lng1, double lat2, double lng2);

S2CellId point_to_cell(double lat, double lng);

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

// --- Thread pool for concurrent admin polygon S2 covering ---

class AdminCoverPool {
public:
    struct WorkItem {
        uint32_t poly_id;
        std::vector<std::pair<double,double>> vertices;
    };

    explicit AdminCoverPool(size_t num_threads);
    ~AdminCoverPool();

    void submit(uint32_t poly_id, std::vector<std::pair<double,double>>&& vertices);
    std::unordered_map<uint64_t, std::vector<uint32_t>> drain();

private:
    void worker_loop();

    std::vector<std::thread> workers_;
    std::deque<WorkItem> queue_;
    std::vector<std::unordered_map<uint64_t, std::vector<uint32_t>>> thread_results_;
    std::mutex mutex_;
    std::condition_variable cv_;
    std::condition_variable done_cv_;
    size_t active_workers_ = 0;
    bool stop_;
};

// --- Simplification dispatch ---

// Simplify a polygon using the current global mode, taking admin_level into account.
// In MaxVertices mode: caps at MAX_POLYGON_VERTICES (500).
// In ErrorBounded mode: uses per-level epsilon in meters, converted to degrees.
inline std::vector<std::pair<double,double>> simplify_admin_polygon(
    const std::vector<std::pair<double,double>>& pts, uint8_t admin_level) {
    if (kSimplifyMode == SimplifyMode::ErrorBounded) {
        double eps_m = kSimplifyEpsilonOverride > 0
            ? kSimplifyEpsilonOverride
            : admin_epsilon_meters(admin_level);
        // Estimate latitude from first vertex for degree conversion
        double lat = pts.empty() ? 0.0 : pts[0].first;
        double eps_deg = meters_to_degrees(eps_m, lat);
        return simplify_polygon_epsilon(pts, eps_deg);
    }
    return simplify_polygon(pts);
}

// --- Helper functions that depend on S2 ---

void add_addr_point(ParsedData& data, double lat, double lng,
                    const char* housenumber, const char* street,
                    uint64_t& addr_count_total);

void add_admin_polygon(ParsedData& data,
                       const std::vector<std::pair<double,double>>& vertices_in,
                       const char* name, uint8_t admin_level,
                       const char* country_code,
                       AdminCoverPool* admin_pool = nullptr);


     1→#include "s2_helpers.h"
     2→#include "geometry.h"
     3→
     4→#include <algorithm>
     5→#include <cmath>
     6→#include <iostream>
     7→#include <memory>
     8→
     9→#include <s2/s2latlng.h>
    10→#include <s2/s2region_coverer.h>
    11→#include <s2/s2polyline.h>
    12→#include <s2/s2polygon.h>
    13→#include <s2/s2loop.h>
    14→
    15→// --- S2 cell level globals ---
    16→
    17→int kStreetCellLevel = 17;
    18→int kAdminCellLevel = 10;
    19→int kMaxAdminLevel = 0;  // 0 means no filtering
    20→
    21→SimplifyMode kSimplifyMode = SimplifyMode::MaxVertices;
    22→double kSimplifyEpsilonOverride = 0;
    23→
    24→// --- cover_edge ---
    25→
    26→void cover_edge(double lat1, double lng1, double lat2, double lng2,
    27→                std::vector<S2CellId>& out) {
    28→    out.clear();
    29→
    30→    S2CellId c1 = S2CellId(S2LatLng::FromDegrees(lat1, lng1)).parent(kStreetCellLevel);
    31→    S2CellId c2 = S2CellId(S2LatLng::FromDegrees(lat2, lng2)).parent(kStreetCellLevel);
    32→
    33→    // Same cell — most common case for short edges
    34→    if (c1 == c2) {
    35→        out.push_back(c1);
    36→        return;
    37→    }
    38→
    39→    // Adjacent cells — check midpoint
    40→    S2CellId cm = S2CellId(S2LatLng::FromDegrees((lat1+lat2)/2, (lng1+lng2)/2)).parent(kStreetCellLevel);
    41→    if (cm == c1 || cm == c2) {
    42→        out.push_back(c1);
    43→        out.push_back(c2);
    44→        return;
    45→    }
    46→
    47→    // Longer edge — use S2RegionCoverer for correctness (handles geodesic curves)
    48→    thread_local S2RegionCoverer coverer = []() {
    49→        S2RegionCoverer::Options options;
    50→        options.set_fixed_level(kStreetCellLevel);
    51→        return S2RegionCoverer(options);
    52→    }();
    53→
    54→    S2Point p1 = S2LatLng::FromDegrees(lat1, lng1).ToPoint();
    55→    S2Point p2 = S2LatLng::FromDegrees(lat2, lng2).ToPoint();
    56→    if (p1 == p2) { out.push_back(c1); return; }
    57→
    58→    std::vector<S2Point> points = {p1, p2};
    59→    S2Polyline polyline(points);
    60→    S2CellUnion covering = coverer.GetCovering(polyline);
    61→    out = std::move(covering.cell_ids());
    62→}
    63→
    64→std::vector<S2CellId> cover_edge(double lat1, double lng1, double lat2, double lng2) {
    65→    std::vector<S2CellId> result;
    66→    cover_edge(lat1, lng1, lat2, lng2, result);
    67→    return result;
    68→}
    69→
    70→S2CellId point_to_cell(double lat, double lng) {
    71→    return S2CellId(S2LatLng::FromDegrees(lat, lng)).parent(kStreetCellLevel);
    72→}
    73→
    74→// --- cover_polygon ---
    75→
    76→std::vector<std::pair<S2CellId, bool>> cover_polygon(
    77→    const std::vector<std::pair<double,double>>& vertices) {
    78→    std::vector<S2Point> points;
    79→    points.reserve(vertices.size());
    80→    for (const auto& [lat, lng] : vertices) {
    81→        S2Point p = S2LatLng::FromDegrees(lat, lng).ToPoint();
    82→        if (!points.empty() && points.back() == p) continue;
    83→        points.push_back(p);
    84→    }
    85→    if (points.size() > 1 && points.front() == points.back()) {
    86→        points.pop_back();
    87→    }
    88→    if (points.size() < 3) return {};
    89→
    90→    S2Error error;
    91→    auto loop = std::make_unique<S2Loop>(points, S2Debug::DISABLE);
    92→    loop->Normalize();
    93→    if (loop->FindValidationError(&error)) return {};
    94→
    95→    S2Polygon polygon(std::move(loop));
    96→
    97→    S2RegionCoverer::Options options;
    98→    options.set_max_level(kAdminCellLevel);
    99→    options.set_max_cells(MAX_S2_CELLS_PER_POLY);
   100→

#include "s2_helpers.h"
#include "geometry.h"

#include <algorithm>
#include <cmath>
#include <iostream>
#include <memory>

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

// --- S2 cell level globals ---

int kStreetCellLevel = 17;
int kAdminCellLevel = 10;
int kMaxAdminLevel = 0;  // 0 means no filtering

SimplifyMode kSimplifyMode = SimplifyMode::MaxVertices;
double kSimplifyEpsilonOverride = 0;

// --- cover_edge ---

void cover_edge(double lat1, double lng1, double lat2, double lng2,
                std::vector<S2CellId>& out) {
    out.clear();

    S2CellId c1 = S2CellId(S2LatLng::FromDegrees(lat1, lng1)).parent(kStreetCellLevel);
    S2CellId c2 = S2CellId(S2LatLng::FromDegrees(lat2, lng2)).parent(kStreetCellLevel);

    // Same cell — most common case for short edges
    if (c1 == c2) {
        out.push_back(c1);
        return;
    }

    // Adjacent cells — check midpoint
    S2CellId cm = S2CellId(S2LatLng::FromDegrees((lat1+lat2)/2, (lng1+lng2)/2)).parent(kStreetCellLevel);
    if (cm == c1 || cm == c2) {
        out.push_back(c1);
        out.push_back(c2);
        return;
    }

    // Longer edge — use S2RegionCoverer for correctness (handles geodesic curves)
    thread_local S2RegionCoverer coverer = []() {
        S2RegionCoverer::Options options;
        options.set_fixed_level(kStreetCellLevel);
        return S2RegionCoverer(options);
    }();

    S2Point p1 = S2LatLng::FromDegrees(lat1, lng1).ToPoint();
    S2Point p2 = S2LatLng::FromDegrees(lat2, lng2).ToPoint();
    if (p1 == p2) { out.push_back(c1); return; }

    std::vector<S2Point> points = {p1, p2};
    S2Polyline polyline(points);
    S2CellUnion covering = coverer.GetCovering(polyline);
    out = std::move(covering.cell_ids());
}

std::vector<S2CellId> cover_edge(double lat1, double lng1, double lat2, double lng2) {
    std::vector<S2CellId> result;
    cover_edge(lat1, lng1, lat2, lng2, result);
    return result;
}

S2CellId point_to_cell(double lat, double lng) {
    return S2CellId(S2LatLng::FromDegrees(lat, lng)).parent(kStreetCellLevel);
}

// --- cover_polygon ---

std::vector<std::pair<S2CellId, bool>> cover_polygon(
    const std::vector<std::pair<double,double>>& vertices) {
    std::vector<S2Point> points;
    points.reserve(vertices.size());
    for (const auto& [lat, lng] : vertices) {
        S2Point p = S2LatLng::FromDegrees(lat, lng).ToPoint();
        if (!points.empty() && points.back() == p) continue;
        points.push_back(p);
    }
    if (points.size() > 1 && points.front() == points.back()) {
        points.pop_back();
    }
    if (points.size() < 3) return {};

    S2Error error;
    auto loop = std::make_unique<S2Loop>(points, S2Debug::DISABLE);
    loop->Normalize();
    if (loop->FindValidationError(&error)) return {};

    S2Polygon polygon(std::move(loop));

    S2RegionCoverer::Options options;
    options.set_max_level(kAdminCellLevel);
    options.set_max_cells(MAX_S2_CELLS_PER_POLY);


Now let me look at how the sorted pairs and cell index are built.


  1000→                            size_t i = way_idx.fetch_add(1);
  1001→                            if (i >= data.deferred_ways.size()) break;
  1002→                            const auto& dw = data.deferred_ways[i];
  1003→                            way_cells.clear();
  1004→                            for (uint8_t j = 0; j + 1 < dw.node_count; j++) {
  1005→                                const auto& n1 = data.street_nodes[dw.node_offset + j];
  1006→                                const auto& n2 = data.street_nodes[dw.node_offset + j + 1];
  1007→                                cover_edge(n1.lat, n1.lng, n2.lat, n2.lng, edge_cells);
  1008→                                for (const auto& c : edge_cells) way_cells.push_back(c.id());
  1009→                            }
  1010→                            std::sort(way_cells.begin(), way_cells.end());
  1011→                            way_cells.erase(std::unique(way_cells.begin(), way_cells.end()), way_cells.end());
  1012→                            for (uint64_t cell_id : way_cells) {
  1013→                                local.push_back({cell_id, dw.way_id});
  1014→                            }
  1015→                        }
  1016→                    });
  1017→                }
  1018→                for (auto& t : threads) t.join();
  1019→            }
  1020→            log_phase("  S2: street ways (parallel)", _s2t, _s2cpu);
  1021→
  1022→            // Process interpolations: emit (cell_id, interp_id) pairs
  1023→            std::cerr << "  Processing " << data.deferred_interps.size() << " interpolation ways..." << std::endl;
  1024→            std::vector<std::vector<CellItem>> interp_pairs(num_threads);
  1025→            {
  1026→                std::atomic<size_t> interp_idx{0};
  1027→                std::vector<std::thread> threads;
  1028→                for (unsigned int t = 0; t < num_threads; t++) {
  1029→                    threads.emplace_back([&, t]() {
  1030→                        auto& local = interp_pairs[t];
  1031→                        std::vector<S2CellId> edge_cells;
  1032→                        std::vector<uint64_t> way_cells;
  1033→                        while (true) {
  1034→                            size_t i = interp_idx.fetch_add(1);
  1035→                            if (i >= data.deferred_interps.size()) break;
  1036→                            const auto& di = data.deferred_interps[i];
  1037→                            way_cells.clear();
  1038→                            for (uint8_t j = 0; j + 1 < di.node_count; j++) {
  1039→                                const auto& n1 = data.interp_nodes[di.node_offset + j];
  1040→                                const auto& n2 = data.interp_nodes[di.node_offset + j + 1];
  1041→                                cover_edge(n1.lat, n1.lng, n2.lat, n2.lng, edge_cells);
  1042→                                for (const auto& c : edge_cells) way_cells.push_back(c.id());
  1043→                            }
  1044→                            std::sort(way_cells.begin(), way_cells.end());
  1045→                            way_cells.erase(std::unique(way_cells.begin(), way_cells.end()), way_cells.end());
  1046→                            for (uint64_t cell_id : way_cells) {
  1047→                                local.push_back({cell_id, di.interp_id});
  1048→                            }
  1049→                        }
  1050→                    });
  1051→                }
  1052→                for (auto& t : threads) t.join();
  1053→            }
  1054→            log_phase("  S2: interp ways (parallel)", _s2t, _s2cpu);
  1055→
  1056→            // Merge thread-local pairs into single vectors, sort, build cell maps
  1057→            std::cerr << "  Sorting and grouping cell pairs..." << std::endl;
  1058→            // Concatenate + sort helper for flat CellItem pairs
  1059→            // Parallel sort each thread's pairs, then k-way merge directly into
  1060→            // hash map + sorted output. No intermediate merged vector needed.
  1061→            auto parallel_sort_and_build = [](
  1062→                std::vector<std::vector<CellItem>>& thread_pairs,
  1063→                std::unordered_map<uint64_t, std::vector<uint32_t>>& cell_map,
  1064→                std::vector<CellItemPair>& sorted_out
  1065→            ) {
  1066→                // Step 1: Convert + sort each thread's data in parallel
  1067→                size_t total = 0;
  1068→                for (auto& v : thread_pairs) total += v.size();
  1069→
  1070→                std::vector<std::vector<CellItemPair>> chunks(thread_pairs.size());
  1071→                {
  1072→                    std::vector<std::thread> sort_threads;
  1073→                    for (size_t t = 0; t < thread_pairs.size(); t++) {
  1074→                        sort_threads.emplace_back([&, t]() {
  1075→                            auto& src = thread_pairs[t];
  1076→                            auto& dst = chunks[t];
  1077→                            dst.reserve(src.size());
  1078→                            for (auto& ci : src) dst.push_back({ci.cell_id, ci.item_id});
  1079→                            src.clear(); src.shrink_to_fit();
  1080→                            std::sort(dst.begin(), dst.end(), [](const CellItemPair& a, const CellItemPair& b) {
  1081→                                return a.cell_id < b.cell_id || (a.cell_id == b.cell_id && a.item_id < b.item_id);
  1082→                            });
  1083→                        });
  1084→                    }
  1085→                    for (auto& t : sort_threads) t.join();
  1086→                }
  1087→
  1088→                // Step 2: Concatenate sorted chunks and merge using parallel tree.
  1089→                // Each chunk is already sorted. Record run boundaries, concatenate,
  1090→                // then merge adjacent runs pairwise in parallel at each level.
  1091→                auto cmp = [](const CellItemPair& a, const CellItemPair& b) {
  1092→                    return a.cell_id < b.cell_id || (a.cell_id == b.cell_id && a.item_id < b.item_id);
  1093→                };
  1094→
  1095→                // Record run boundaries before concatenation
  1096→                std::vector<size_t> run_bounds = {0};
  1097→                sorted_out.clear();
  1098→                sorted_out.reserve(total);
  1099→                for (auto& chunk : chunks) {
  1100→                    sorted_out.insert(sorted_out.end(), chunk.begin(), chunk.end());
  1101→                    run_bounds.push_back(sorted_out.size());
  1102→                    chunk.clear(); chunk.shrink_to_fit();
  1103→                }
  1104→
  1105→                // Parallel tree merge: at each level, merge adjacent run pairs in parallel.
  1106→                // Level 0: merge runs (0,1), (2,3), ... → N/2 runs
  1107→                // Level 1: merge runs (01,23), (45,67), ... → N/4 runs
  1108→                // ... until one sorted run remains.
  1109→                while (run_bounds.size() > 2) {
  1110→                    std::vector<size_t> new_bounds = {0};
  1111→                    std::vector<std::thread> merge_threads;
  1112→                    for (size_t i = 0; i + 2 < run_bounds.size(); i += 2) {
  1113→                        size_t left = run_bounds[i];
  1114→                        size_t mid = run_bounds[i + 1];
  1115→                        size_t right = run_bounds[i + 2];
  1116→                        merge_threads.emplace_back([&, left, mid, right] {
  1117→                            std::inplace_merge(sorted_out.begin() + left,
  1118→                                               sorted_out.begin() + mid,
  1119→                                               sorted_out.begin() + right, cmp);
  1120→                        });
  1121→                        new_bounds.push_back(right);
  1122→                    }
  1123→                    // If odd number of runs, carry the last one forward
  1124→                    if (run_bounds.size() % 2 == 0) {
  1125→                        new_bounds.push_back(run_bounds.back());
  1126→                    }
  1127→                    for (auto& t : merge_threads) t.join();
  1128→                    run_bounds = std::move(new_bounds);
  1129→                }
  1130→
  1131→                // Skip building hash map — sorted_out is used directly for writing.
  1132→                // cell_map stays empty (only needed for cache/continent modes).
  1133→            };
  1134→
  1135→
  1136→            // Sort each thread's pairs in parallel, then k-way merge directly
  1137→            // into hash map + sorted output. No intermediate merged vector.
  1138→            auto f_ways = std::async(std::launch::async, [&] {
  1139→                parallel_sort_and_build(way_pairs, data.cell_to_ways, data.sorted_way_cells);
  1140→            });
  1141→            auto f_interps = std::async(std::launch::async, [&] {
  1142→                parallel_sort_and_build(interp_pairs, data.cell_to_interps, data.sorted_interp_cells);
  1143→            });
  1144→            f_ways.get();
  1145→            f_interps.get();
  1146→            log_phase("  S2: sort + group into cell maps", _s2t, _s2cpu);
  1147→
  1148→            // Free deferred work items
  1149→            data.deferred_ways.clear();
  1150→            data.deferred_ways.shrink_to_fit();
  1151→            data.deferred_interps.clear();
  1152→            data.deferred_interps.shrink_to_fit();
  1153→
  1154→            std::cerr << "S2 cell computation complete." << std::endl;
  1155→        }
  1156→
  1157→        // Resolve interpolation endpoints
  1158→        std::cerr << "Resolving interpolation endpoints..." << std::endl;
  1159→        resolve_interpolation_endpoints(data);
  1160→
  1161→        // Deduplicate + convert to sorted pairs for fast writing
  1162→        log_phase("S2 cell computation", _pt, _cpu);
  1163→        std::cerr << "Deduplicating + sorting for write..." << std::endl;
  1164→        {
  1165→            // Convert addr hash map to sorted pairs.
  1166→            // Sort cell IDs only (30M unique), then build pairs in sorted order.
  1167→            // Much faster than sorting all 160M pairs.
  1168→            auto f2 = std::async(std::launch::async, [&] {
  1169→                auto _dt = std::chrono::steady_clock::now();
  1170→                auto _dc = CpuTicks::now();
  1171→
  1172→                // Step 1: Extract cell IDs from hash map
  1173→                std::vector<uint64_t> sorted_cells;
  1174→                sorted_cells.reserve(data.cell_to_addrs.size());
  1175→                for (auto& [cell_id, ids] : data.cell_to_addrs)
  1176→                    sorted_cells.push_back(cell_id);
  1177→                log_phase("    Dedup: extract cell IDs", _dt, _dc);
  1178→
  1179→                // Step 2: Sort cell IDs
  1180→                std::sort(sorted_cells.begin(), sorted_cells.end());
  1181→                log_phase("    Dedup: sort cell IDs", _dt, _dc);
  1182→
  1183→                // Step 3: Parallel dedup within each cell
  1184→                {
  1185→                    unsigned nthreads = std::thread::hardware_concurrency();
  1186→                    size_t chunk = (sorted_cells.size() + nthreads - 1) / nthreads;
  1187→                    std::vector<std::thread> threads;
  1188→                    for (unsigned t = 0; t < nthreads; t++) {
  1189→                        size_t start = t * chunk;
  1190→                        size_t end = std::min(start + chunk, sorted_cells.size());
  1191→                        if (start >= sorted_cells.size()) break;
  1192→                        threads.emplace_back([&, start, end]() {
  1193→                            for (size_t i = start; i < end; i++) {
  1194→                                auto& ids = data.cell_to_addrs[sorted_cells[i]];
  1195→                                std::sort(ids.begin(), ids.end());
  1196→                                ids.erase(std::unique(ids.begin(), ids.end()), ids.end());
  1197→                            }
  1198→                        });
  1199→                    }

                            size_t i = way_idx.fetch_add(1);
                            if (i >= data.deferred_ways.size()) break;
                            const auto& dw = data.deferred_ways[i];
                            way_cells.clear();
                            for (uint8_t j = 0; j + 1 < dw.node_count; j++) {
                                const auto& n1 = data.street_nodes[dw.node_offset + j];
                                const auto& n2 = data.street_nodes[dw.node_offset + j + 1];
                                cover_edge(n1.lat, n1.lng, n2.lat, n2.lng, edge_cells);
                                for (const auto& c : edge_cells) way_cells.push_back(c.id());
                            }
                            std::sort(way_cells.begin(), way_cells.end());
                            way_cells.erase(std::unique(way_cells.begin(), way_cells.end()), way_cells.end());
                            for (uint64_t cell_id : way_cells) {
                                local.push_back({cell_id, dw.way_id});
                            }
                        }
                    });
                }
                for (auto& t : threads) t.join();
            }
            log_phase("  S2: street ways (parallel)", _s2t, _s2cpu);

            // Process interpolations: emit (cell_id, interp_id) pairs
            std::cerr << "  Processing " << data.deferred_interps.size() << " interpolation ways..." << std::endl;
            std::vector<std::vector<CellItem>> interp_pairs(num_threads);
            {
                std::atomic<size_t> interp_idx{0};
                std::vector<std::thread> threads;
                for (unsigned int t = 0; t < num_threads; t++) {
                    threads.emplace_back([&, t]() {
                        auto& local = interp_pairs[t];
                        std::vector<S2CellId> edge_cells;
                        std::vector<uint64_t> way_cells;
                        while (true) {
                            size_t i = interp_idx.fetch_add(1);
                            if (i >= data.deferred_interps.size()) break;
                            const auto& di = data.deferred_interps[i];
                            way_cells.clear();
                            for (uint8_t j = 0; j + 1 < di.node_count; j++) {
                                const auto& n1 = data.interp_nodes[di.node_offset + j];
                                const auto& n2 = data.interp_nodes[di.node_offset + j + 1];
                                cover_edge(n1.lat, n1.lng, n2.lat, n2.lng, edge_cells);
                                for (const auto& c : edge_cells) way_cells.push_back(c.id());
                            }
                            std::sort(way_cells.begin(), way_cells.end());
                            way_cells.erase(std::unique(way_cells.begin(), way_cells.end()), way_cells.end());
                            for (uint64_t cell_id : way_cells) {
                                local.push_back({cell_id, di.interp_id});
                            }
                        }
                    });
                }
                for (auto& t : threads) t.join();
            }
            log_phase("  S2: interp ways (parallel)", _s2t, _s2cpu);

            // Merge thread-local pairs into single vectors, sort, build cell maps
            std::cerr << "  Sorting and grouping cell pairs..." << std::endl;
            // Concatenate + sort helper for flat CellItem pairs
            // Parallel sort each thread's pairs, then k-way merge directly into
            // hash map + sorted output. No intermediate merged vector needed.
            auto parallel_sort_and_build = [](
                std::vector<std::vector<CellItem>>& thread_pairs,
                std::unordered_map<uint64_t, std::vector<uint32_t>>& cell_map,
                std::vector<CellItemPair>& sorted_out
            ) {
                // Step 1: Convert + sort each thread's data in parallel
                size_t total = 0;
                for (auto& v : thread_pairs) total += v.size();

                std::vector<std::vector<CellItemPair>> chunks(thread_pairs.size());
                {
                    std::vector<std::thread> sort_threads;
                    for (size_t t = 0; t < thread_pairs.size(); t++) {
                        sort_threads.emplace_back([&, t]() {
                            auto& src = thread_pairs[t];
                            auto& dst = chunks[t];
                            dst.reserve(src.size());
                            for (auto& ci : src) dst.push_back({ci.cell_id, ci.item_id});
                            src.clear(); src.shrink_to_fit();
                            std::sort(dst.begin(), dst.end(), [](const CellItemPair& a, const CellItemPair& b) {
                                return a.cell_id < b.cell_id || (a.cell_id == b.cell_id && a.item_id < b.item_id);
                            });
                        });
                    }
                    for (auto& t : sort_threads) t.join();
                }

                // Step 2: Concatenate sorted chunks and merge using parallel tree.
                // Each chunk is already sorted. Record run boundaries, concatenate,
                // then merge adjacent runs pairwise in parallel at each level.
                auto cmp = [](const CellItemPair& a, const CellItemPair& b) {
                    return a.cell_id < b.cell_id || (a.cell_id == b.cell_id && a.item_id < b.item_id);
                };

                // Record run boundaries before concatenation
                std::vector<size_t> run_bounds = {0};
                sorted_out.clear();
                sorted_out.reserve(total);
                for (auto& chunk : chunks) {
                    sorted_out.insert(sorted_out.end(), chunk.begin(), chunk.end());
                    run_bounds.push_back(sorted_out.size());
                    chunk.clear(); chunk.shrink_to_fit();
                }

                // Parallel tree merge: at each level, merge adjacent run pairs in parallel.
                // Level 0: merge runs (0,1), (2,3), ... → N/2 runs
                // Level 1: merge runs (01,23), (45,67), ... → N/4 runs
                // ... until one sorted run remains.
                while (run_bounds.size() > 2) {
                    std::vector<size_t> new_bounds = {0};
                    std::vector<std::thread> merge_threads;
                    for (size_t i = 0; i + 2 < run_bounds.size(); i += 2) {
                        size_t left = run_bounds[i];
                        size_t mid = run_bounds[i + 1];
                        size_t right = run_bounds[i + 2];
                        merge_threads.emplace_back([&, left, mid, right] {
                            std::inplace_merge(sorted_out.begin() + left,
                                               sorted_out.begin() + mid,
                                               sorted_out.begin() + right, cmp);
                        });
                        new_bounds.push_back(right);
                    }
                    // If odd number of runs, carry the last one forward
                    if (run_bounds.size() % 2 == 0) {
                        new_bounds.push_back(run_bounds.back());
                    }
                    for (auto& t : merge_threads) t.join();
                    run_bounds = std::move(new_bounds);
                }

                // Skip building hash map — sorted_out is used directly for writing.
                // cell_map stays empty (only needed for cache/continent modes).
            };

            // Sort each thread's pairs in parallel, then k-way merge directly
            // into hash map + sorted output. No intermediate merged vector.
            auto f_ways = std::async(std::launch::async, [&] {
                parallel_sort_and_build(way_pairs, data.cell_to_ways, data.sorted_way_cells);
            });
            auto f_interps = std::async(std::launch::async, [&] {
                parallel_sort_and_build(interp_pairs, data.cell_to_interps, data.sorted_interp_cells);
            });
            f_ways.get();
            f_interps.get();
            log_phase("  S2: sort + group into cell maps", _s2t, _s2cpu);

            // Free deferred work items
            data.deferred_ways.clear();
            data.deferred_ways.shrink_to_fit();
            data.deferred_interps.clear();
            data.deferred_interps.shrink_to_fit();

            std::cerr << "S2 cell computation complete." << std::endl;
        }

        // Resolve interpolation endpoints
        std::cerr << "Resolving interpolation endpoints..." << std::endl;
        resolve_interpolation_endpoints(data);

        // Deduplicate + convert to sorted pairs for fast writing
        log_phase("S2 cell computation", _pt, _cpu);
        std::cerr << "Deduplicating + sorting for write..." << std::endl;
        {
            // Convert addr hash map to sorted pairs.
            // Sort cell IDs only (30M unique), then build pairs in sorted order.
            // Much faster than sorting all 160M pairs.
            auto f2 = std::async(std::launch::async, [&] {
                auto _dt = std::chrono::steady_clock::now();
                auto _dc = CpuTicks::now();

                // Step 1: Extract cell IDs from hash map
                std::vector<uint64_t> sorted_cells;
                sorted_cells.reserve(data.cell_to_addrs.size());
                for (auto& [cell_id, ids] : data.cell_to_addrs)
                    sorted_cells.push_back(cell_id);
                log_phase("    Dedup: extract cell IDs", _dt, _dc);

                // Step 2: Sort cell IDs
                std::sort(sorted_cells.begin(), sorted_cells.end());
                log_phase("    Dedup: sort cell IDs", _dt, _dc);

                // Step 3: Parallel dedup within each cell
                {
                    unsigned nthreads = std::thread::hardware_concurrency();
                    size_t chunk = (sorted_cells.size() + nthreads - 1) / nthreads;
                    std::vector<std::thread> threads;
                    for (unsigned t = 0; t < nthreads; t++) {
                        size_t start = t * chunk;
                        size_t end = std::min(start + chunk, sorted_cells.size());
                        if (start >= sorted_cells.size()) break;
                        threads.emplace_back([&, start, end]() {
                            for (size_t i = start; i < end; i++) {
                                auto& ids = data.cell_to_addrs[sorted_cells[i]];
                                std::sort(ids.begin(), ids.end());
                                ids.erase(std::unique(ids.begin(), ids.end()), ids.end());
                            }
                        });
                    }