  2400→
  2401→                                    // Handle closed ways that are admin boundaries themselves
  2402→                                    // Osmium creates areas from closed ways independently of
  2403→                                    // multipolygon relations, even if the way is a relation member.
  2404→                                    {
  2405→                                        const char* boundary = way.tags()["boundary"];
  2406→                                        if (boundary) {
  2407→                                            bool is_admin = (std::strcmp(boundary, "administrative") == 0);
  2408→                                            bool is_postal = (std::strcmp(boundary, "postal_code") == 0);
  2409→                                            if ((is_admin || is_postal) && wnodes.size() >= 4 &&
  2410→                                                wnodes.front().ref() == wnodes.back().ref()) {
  2411→                                                // Closed way forming an admin polygon
  2412→                                                uint8_t al = 0;
  2413→                                                if (is_admin) {
  2414→                                                    const char* level_str = way.tags()["admin_level"];
  2415→                                                    if (level_str) al = static_cast<uint8_t>(std::atoi(level_str));
  2416→                                                } else {
  2417→                                                    al = 11;
  2418→                                                }
  2419→                                                int max_al = is_postal ? 11 : 10;
  2420→                                                if (al >= 2 && al <= max_al && (kMaxAdminLevel == 0 || al <= kMaxAdminLevel)) {
  2421→                                                    const char* aname = way.tags()["name"];
  2422→                                                    if (!aname && is_admin) continue; // admin needs name
  2423→                                                    std::string name_str;
  2424→                                                    if (is_postal) {
  2425→                                                        const char* pc = way.tags()["postal_code"];
  2426→                                                        if (!pc) pc = aname;
  2427→                                                        if (!pc) continue; // postal needs postal_code or name
  2428→                                                        name_str = pc;
  2429→                                                    } else {
  2430→                                                        name_str = aname;
  2431→                                                    }
  2432→                                                    {
  2433→                                                        std::vector<std::pair<double,double>> verts;
  2434→                                                        bool all_valid = true;
  2435→                                                        for (const auto& nr : wnodes) {
  2436→                                                            try {
  2437→                                                                auto loc = index.get(nr.positive_ref());
  2438→                                                                if (loc.valid()) {
  2439→                                                                    verts.push_back({loc.lat(), loc.lon()});
  2440→                                                                } else { all_valid = false; break; }
  2441→                                                            } catch (...) { all_valid = false; break; }
  2442→                                                        }
  2443→                                                        if (all_valid && verts.size() >= 3) {
  2444→                                                            std::string cc;
  2445→                                                            if (al == 2) {
  2446→                                                                const char* iso = way.tags()["ISO3166-1:alpha2"];
  2447→                                                                if (iso) cc = iso;
  2448→                                                            }
  2449→                                                            local.closed_way_admins.push_back({
  2450→                                                                std::move(verts),
  2451→                                                                std::move(name_str),
  2452→                                                                al, std::move(cc)
  2453→                                                            });
  2454→                                                        }
  2455→                                                    }
  2456→                                                }
  2457→                                            }
  2458→                                        }
  2459→                                    }
  2460→                                }
  2461→                            }
  2462→
  2463→                            uint64_t done = way_blocks_done.fetch_add(1) + 1;
  2464→                            if (done % 1000 == 0) {
  2465→                                std::cerr << "  Processed " << done << " way blocks..." << std::endl;
  2466→                            }
  2467→                        }
  2468→                    });
  2469→                }
  2470→
  2471→                // Way reader: use the combined_reader (continuing from where nodes left off)
  2472→                {
  2473→                    // First, push the stashed way buffer
  2474→                    if (first_way_buf) {
  2475→                        std::unique_lock<std::mutex> lock(way_queue_mutex);
  2476→                        way_queue_cv.wait(lock, [&]{ return way_block_queue.size() < WAY_MAX_QUEUE; });
  2477→                        way_block_queue.push_back(std::move(first_way_buf));
  2478→                        way_queue_cv.notify_one();
  2479→                    }
  2480→                    // Continue reading remaining blocks from the same reader
  2481→                    while (auto buf = combined_reader.read()) {
  2482→                        {
  2483→                            std::unique_lock<std::mutex> lock(way_queue_mutex);
  2484→                            way_queue_cv.wait(lock, [&]{ return way_block_queue.size() < WAY_MAX_QUEUE; });
  2485→                            way_block_queue.push_back(std::move(buf));
  2486→                        }
  2487→                        way_queue_cv.notify_one();
  2488→                    }
  2489→                    combined_reader.close();
  2490→                    {
  2491→                        std::lock_guard<std::mutex> lock(way_queue_mutex);
  2492→                        way_reader_done = true;
  2493→                    }
  2494→                    way_queue_cv.notify_all();
  2495→                }
  2496→
  2497→                for (auto& w : workers) w.join();
  2498→                std::cerr << "  Parallel way processing complete." << std::endl;
  2499→
  2500→                // Process areas/multipolygons — sequential fallback path
  2501→                // Merge thread-local way/interp data into main ParsedData
  2502→                std::cerr << "  Merging thread-local data..." << std::endl;
  2503→                uint64_t total_ways = 0, total_building_addrs = 0, total_interps = 0;
  2504→                for (auto& local : tld) {
  2505→                    uint32_t way_base = static_cast<uint32_t>(data.ways.size());
  2506→                    uint32_t node_base = static_cast<uint32_t>(data.street_nodes.size());
  2507→                    uint32_t interp_base = static_cast<uint32_t>(data.interp_ways.size());
  2508→                    uint32_t interp_node_base = static_cast<uint32_t>(data.interp_nodes.size());
  2509→
  2510→                    // Merge street ways
  2511→                    for (size_t i = 0; i < local.ways.size(); i++) {
  2512→                        auto h = local.ways[i];
  2513→                        h.node_offset += node_base;
  2514→                        h.name_id = data.string_pool.intern(local.way_strings[i]);
  2515→                        data.ways.push_back(h);
  2516→                    }
  2517→                    data.street_nodes.insert(data.street_nodes.end(),
  2518→                        local.street_nodes.begin(), local.street_nodes.end());
  2519→
  2520→                    // Remap deferred ways
  2521→                    for (auto dw : local.deferred_ways) {
  2522→                        dw.way_id += way_base;
  2523→                        dw.node_offset += node_base;
  2524→                        data.deferred_ways.push_back(dw);
  2525→                    }
  2526→
  2527→                    // Merge building addresses
  2528→                    for (size_t i = 0; i < local.building_addrs.size(); i++) {
  2529→                        uint64_t dummy = 0;
  2530→                        add_addr_point(data, local.building_addrs[i].lat, local.building_addrs[i].lng,
  2531→                                       local.addr_strings[i].first.c_str(),
  2532→                                       local.addr_strings[i].second.c_str(), dummy);
  2533→                    }
  2534→
  2535→                    // Merge interpolation ways
  2536→                    for (size_t i = 0; i < local.interp_ways.size(); i++) {
  2537→                        auto iw = local.interp_ways[i];
  2538→                        iw.node_offset += interp_node_base;
  2539→                        iw.street_id = data.string_pool.intern(local.interp_strings[i]);
  2540→                        data.interp_ways.push_back(iw);
  2541→                    }
  2542→                    data.interp_nodes.insert(data.interp_nodes.end(),
  2543→                        local.interp_nodes.begin(), local.interp_nodes.end());
  2544→
  2545→                    // Remap deferred interps
  2546→                    for (auto di : local.deferred_interps) {
  2547→                        di.interp_id += interp_base;
  2548→                        di.node_offset += interp_node_base;
  2549→                        data.deferred_interps.push_back(di);
  2550→                    }
  2551→
  2552→                    total_ways += local.way_count;
  2553→                    total_building_addrs += local.building_addr_count;
  2554→                    total_interps += local.interp_count;
  2555→
  2556→                    // Merge way geometries for admin assembly
  2557→                    {
  2558→                        for (auto& wg : local.way_geoms) {
  2559→                            ParsedData::WayGeometry g;
  2560→                            g.coords = std::move(wg.coords);
  2561→                            g.first_node_id = wg.first_node_id;
  2562→                            g.last_node_id = wg.last_node_id;
  2563→                            data.way_geometries[wg.way_id] = std::move(g);
  2564→                        }
  2565→                        local.way_geoms.clear();
  2566→                        local.way_geoms.shrink_to_fit();
  2567→                    }
  2568→                }
  2569→                std::cerr << "  Merged: " << total_ways << " ways, "
  2570→                          << total_building_addrs << " building addrs, "
  2571→                          << total_interps << " interps from parallel processing." << std::endl;
  2572→
  2573→                // --- Merge closed-way admin polygons ---
  2574→                {
  2575→                    uint64_t closed_way_admin_count = 0;
  2576→                    for (auto& local : tld) {
  2577→                        for (auto& cwa : local.closed_way_admins) {
  2578→                            const char* cc = cwa.country_code.empty() ? nullptr : cwa.country_code.c_str();
  2579→                            add_admin_polygon(data, cwa.vertices, cwa.name.c_str(),
  2580→                                              cwa.admin_level, cc, &admin_pool);
  2581→                            closed_way_admin_count++;
  2582→                        }
  2583→                        local.closed_way_admins.clear();
  2584→                    }
  2585→                    if (closed_way_admin_count > 0) {
  2586→                        std::cerr << "  Added " << closed_way_admin_count
  2587→                                  << " admin polygons from closed ways." << std::endl;
  2588→                    }
  2589→                }
  2590→
  2591→                // --- Parallel admin boundary assembly ---
  2592→                {
  2593→                    log_phase("Pass 2b: way processing", _pt);
  2594→                    std::cerr << "  Assembling admin polygons in parallel ("
  2595→                              << data.collected_relations.size() << " relations, "
  2596→                              << data.way_geometries.size() << " way geometries)..." << std::endl;
  2597→
  2598→                    // Thread-local admin results (to avoid locking add_admin_polygon)
  2599→                    struct AdminResult {
  2600→                        std::vector<std::pair<double,double>> vertices;
  2601→                        std::string name;
  2602→                        uint8_t admin_level;
  2603→                        std::string country_code;
  2604→                    };
  2605→
  2606→                    std::vector<std::vector<AdminResult>> thread_admin_results(num_threads);
  2607→                    std::atomic<size_t> rel_idx{0};
  2608→                    std::atomic<uint64_t> assembled_count{0};
  2609→
  2610→                    std::vector<std::thread> admin_workers;
  2611→                    for (unsigned int t = 0; t < num_threads; t++) {
  2612→                        admin_workers.emplace_back([&, t]() {
  2613→                            auto& local_results = thread_admin_results[t];
  2614→                            while (true) {
  2615→                                size_t i = rel_idx.fetch_add(1);
  2616→                                if (i >= data.collected_relations.size()) break;
  2617→
  2618→                                const auto& rel = data.collected_relations[i];
  2619→
  2620→                                // Skip level 2 border-line relations — these have names like
  2621→                                // "Deutschland - Österreich" (two countries separated by
  2622→                                // " - " or " — "). They are boundary lines, not country
  2623→                                // polygons, and produce self-intersecting geometry.
  2624→                                if (rel.admin_level == 2 && rel.country_code.empty() &&
  2625→                                    (rel.name.find(" - ") != std::string::npos ||
  2626→                                     rel.name.find(" \xe2\x80\x94 ") != std::string::npos)) { // " — " (em dash)
  2627→                                    assembled_count.fetch_add(1);
  2628→                                    continue;
  2629→                                }
  2630→
  2631→                                // Skip relations with missing member ways —
  2632→                                // these cross the extract boundary and would produce
  2633→                                // incorrect partial polygons
  2634→                                bool has_missing = false;
  2635→                                for (const auto& [way_id, role] : rel.members) {
  2636→                                    if (data.way_geometries.find(way_id) == data.way_geometries.end()) {
  2637→                                        has_missing = true;
  2638→                                        break;
  2639→                                    }
  2640→                                }
  2641→                                if (has_missing) {
  2642→                                    assembled_count.fetch_add(1);
  2643→                                    continue;
  2644→                                }
  2645→
  2646→                                auto rings = assemble_outer_rings(rel.members, data.way_geometries);
  2647→                                // If outer-only assembly failed, retry with all ways.
  2648→                                // Osmium ignores roles during assembly (check_roles=false) —
  2649→                                // inner ways sometimes form the boundary or bridge gaps.
  2650→                                // Filter retry results: discard rings with duplicate coords
  2651→                                // (figure-8 shapes from merging holes with outer boundary).
  2652→                                if (rings.empty()) {
  2653→                                    auto retry = assemble_outer_rings(rel.members, data.way_geometries, true);
  2654→                                    auto ck = [](double lat, double lng) -> int64_t {
  2655→                                        int32_t ilat = static_cast<int32_t>(lat * 1e7 + (lat >= 0 ? 0.5 : -0.5));
  2656→                                        int32_t ilng = static_cast<int32_t>(lng * 1e7 + (lng >= 0 ? 0.5 : -0.5));
  2657→                                        return (static_cast<int64_t>(ilat) << 32) | static_cast<uint32_t>(ilng);
  2658→                                    };
  2659→                                    for (auto& ring : retry) {
  2660→                                        if (!ring_has_duplicate_coords(ring, ck)) {
  2661→                                            rings.push_back(std::move(ring));
  2662→                                        }
  2663→                                    }
  2664→                                }
  2665→
  2666→                                // Diagnostic: log relations with 0 rings produced
  2667→                                if (rings.empty() && !rel.members.empty()) {
  2668→                                    int total_ways = 0, found = 0, missing = 0;
  2669→                                    for (const auto& [way_id, role] : rel.members) {
  2670→                                        total_ways++;
  2671→                                        auto it = data.way_geometries.find(way_id);
  2672→                                        if (it != data.way_geometries.end() && !it->second.coords.empty()) {
  2673→                                            found++;
  2674→                                        } else {
  2675→                                            missing++;
  2676→                                        }
  2677→                                    }
  2678→                                    if (missing == 0 && found > 0) {
  2679→                                        std::string detail = "  DEBUG '" + rel.name + "': ways:";
  2680→                                        for (const auto& [way_id, role] : rel.members) {
  2681→                                            auto it = data.way_geometries.find(way_id);
  2682→                                            if (it == data.way_geometries.end()) continue;
  2683→                                            detail += " [w" + std::to_string(way_id) +
  2684→                                                      " first=" + std::to_string(it->second.first_node_id) +
  2685→                                                      " last=" + std::to_string(it->second.last_node_id) +
  2686→                                                      " n=" + std::to_string(it->second.coords.size()) + "]";
  2687→                                        }
  2688→                                        std::cerr << detail << std::endl;
  2689→                                    }
  2690→                                    std::cerr << "  WARN: relation '" << rel.name
  2691→                                              << "' (level " << (int)rel.admin_level
  2692→                                              << ", " << rel.members.size() << " members): "
  2693→                                              << "0 rings from " << total_ways << " ways ("
  2694→                                              << found << " found, " << missing << " missing)" << std::endl;
  2695→                                }
  2696→
  2697→                                for (auto& ring : rings) {
  2698→                                    if (ring.size() >= 3) {
  2699→                                        AdminResult ar;
  2700→                                        ar.vertices = std::move(ring);
  2701→                                        ar.name = rel.name;
  2702→                                        ar.admin_level = rel.admin_level;
  2703→                                        ar.country_code = rel.country_code;
  2704→                                        local_results.push_back(std::move(ar));
  2705→                                    }
  2706→                                }
  2707→
  2708→                                uint64_t done = assembled_count.fetch_add(1) + 1;
  2709→                                if (done % 10000 == 0) {
  2710→                                    std::cerr << "  Assembled " << done / 1000
  2711→                                              << "K admin relations..." << std::endl;
  2712→                                }
  2713→                            }
  2714→                        });
  2715→                    }
  2716→                    for (auto& w : admin_workers) w.join();
  2717→
  2718→                    // Merge thread-local admin results into ParsedData (sequential, thread-safe)
  2719→                    uint64_t total_admin_rings = 0;
  2720→                    for (auto& local_results : thread_admin_results) {
  2721→                        for (auto& ar : local_results) {
  2722→                            const char* cc = ar.country_code.empty() ? nullptr : ar.country_code.c_str();
  2723→                            add_admin_polygon(data, ar.vertices, ar.name.c_str(),
  2724→                                              ar.admin_level, cc, &admin_pool);
  2725→                            total_admin_rings++;
  2726→                        }
  2727→                    }
  2728→                    std::cerr << "  Parallel admin assembly complete: "
  2729→                              << total_admin_rings << " polygon rings from "
  2730→                              << data.collected_relations.size() << " relations." << std::endl;
  2731→
  2732→                    // Free collected data
  2733→                    data.collected_relations.clear();
  2734→                    data.collected_relations.shrink_to_fit();
  2735→                    data.way_geometries.clear();
  2736→                    std::unordered_map<int64_t, ParsedData::WayGeometry>().swap(data.way_geometries);
  2737→                }
  2738→            }
  2739→        }
  2740→
  2741→        std::cerr << "Done reading:" << std::endl;
  2742→        std::cerr << "  " << data.ways.size() << " street ways" << std::endl;
  2743→        std::cerr << "  " << data.addr_points.size() << " address points" << std::endl;
  2744→        std::cerr << "  " << data.interp_ways.size() << " interpolation ways" << std::endl;
  2745→        std::cerr << "  " << data.admin_polygons.size() << " admin polygon rings" << std::endl;
  2746→
  2747→        // Drain admin polygon thread pool (may still be processing)
  2748→        std::cerr << "Waiting for admin polygon S2 covering to complete..." << std::endl;
  2749→        auto admin_results = admin_pool.drain();
  2750→        for (auto& [cell_id, ids] : admin_results) {
  2751→            auto& target = data.cell_to_admin[cell_id];
  2752→            target.insert(target.end(), ids.begin(), ids.end());
  2753→        }
  2754→        std::cerr << "Admin polygon S2 covering complete (" << data.cell_to_admin.size() << " cells)." << std::endl;
  2755→
  2756→        // --- Parallel S2 cell computation for ways and interpolation ---
  2757→        {
  2758→            log_phase("Admin assembly", _pt);
  2759→            std::cerr << "Computing S2 cells for ways with " << num_threads << " threads..." << std::endl;
  2760→
  2761→            // Flat-array approach: threads emit (cell_id, item_id) pairs into
  2762→            // thread-local vectors, then we sort globally and group by cell_id.
  2763→            // This avoids hash maps entirely — sort is cache-friendly O(n log n).
  2764→
  2765→            struct CellItem { uint64_t cell_id; uint32_t item_id; };
  2766→
  2767→            auto _s2t = std::chrono::steady_clock::now();
  2768→
  2769→            // Process streets: emit (cell_id, way_id) pairs
  2770→            std::cerr << "  Processing " << data.deferred_ways.size() << " street ways..." << std::endl;
  2771→            std::vector<std::vector<CellItem>> way_pairs(num_threads);
  2772→            {
  2773→                std::atomic<size_t> way_idx{0};
  2774→                std::vector<std::thread> threads;
  2775→                for (unsigned int t = 0; t < num_threads; t++) {
  2776→                    threads.emplace_back([&, t]() {
  2777→                        auto& local = way_pairs[t];
  2778→                        local.reserve(data.deferred_ways.size() / num_threads * 3);
  2779→                        std::vector<S2CellId> edge_cells;
  2780→                        std::vector<uint64_t> way_cells;
  2781→                        while (true) {
  2782→                            size_t i = way_idx.fetch_add(1);
  2783→                            if (i >= data.deferred_ways.size()) break;
  2784→                            const auto& dw = data.deferred_ways[i];
  2785→                            way_cells.clear();
  2786→                            for (uint8_t j = 0; j + 1 < dw.node_count; j++) {
  2787→                                const auto& n1 = data.street_nodes[dw.node_offset + j];
  2788→                                const auto& n2 = data.street_nodes[dw.node_offset + j + 1];
  2789→                                cover_edge(n1.lat, n1.lng, n2.lat, n2.lng, edge_cells);
  2790→                                for (const auto& c : edge_cells) way_cells.push_back(c.id());
  2791→                            }
  2792→                            std::sort(way_cells.begin(), way_cells.end());
  2793→                            way_cells.erase(std::unique(way_cells.begin(), way_cells.end()), way_cells.end());
  2794→                            for (uint64_t cell_id : way_cells) {
  2795→                                local.push_back({cell_id, dw.way_id});
  2796→                            }
  2797→                        }
  2798→                    });
  2799→                }

                                    // Handle closed ways that are admin boundaries themselves
                                    // Osmium creates areas from closed ways independently of
                                    // multipolygon relations, even if the way is a relation member.
                                    {
                                        const char* boundary = way.tags()["boundary"];
                                        if (boundary) {
                                            bool is_admin = (std::strcmp(boundary, "administrative") == 0);
                                            bool is_postal = (std::strcmp(boundary, "postal_code") == 0);
                                            if ((is_admin || is_postal) && wnodes.size() >= 4 &&
                                                wnodes.front().ref() == wnodes.back().ref()) {
                                                // Closed way forming an admin polygon
                                                uint8_t al = 0;
                                                if (is_admin) {
                                                    const char* level_str = way.tags()["admin_level"];
                                                    if (level_str) al = static_cast<uint8_t>(std::atoi(level_str));
                                                } else {
                                                    al = 11;
                                                }
                                                int max_al = is_postal ? 11 : 10;
                                                if (al >= 2 && al <= max_al && (kMaxAdminLevel == 0 || al <= kMaxAdminLevel)) {
                                                    const char* aname = way.tags()["name"];
                                                    if (!aname && is_admin) continue; // admin needs name
                                                    std::string name_str;
                                                    if (is_postal) {
                                                        const char* pc = way.tags()["postal_code"];
                                                        if (!pc) pc = aname;
                                                        if (!pc) continue; // postal needs postal_code or name
                                                        name_str = pc;
                                                    } else {
                                                        name_str = aname;
                                                    }
                                                    {
                                                        std::vector<std::pair<double,double>> verts;
                                                        bool all_valid = true;
                                                        for (const auto& nr : wnodes) {
                                                            try {
                                                                auto loc = index.get(nr.positive_ref());
                                                                if (loc.valid()) {
                                                                    verts.push_back({loc.lat(), loc.lon()});
                                                                } else { all_valid = false; break; }
                                                            } catch (...) { all_valid = false; break; }
                                                        }
                                                        if (all_valid && verts.size() >= 3) {
                                                            std::string cc;
                                                            if (al == 2) {
                                                                const char* iso = way.tags()["ISO3166-1:alpha2"];
                                                                if (iso) cc = iso;
                                                            }
                                                            local.closed_way_admins.push_back({
                                                                std::move(verts),
                                                                std::move(name_str),
                                                                al, std::move(cc)
                                                            });
                                                        }
                                                    }
                                                }
                                            }
                                        }
                                    }
                                }
                            }

                            uint64_t done = way_blocks_done.fetch_add(1) + 1;
                            if (done % 1000 == 0) {
                                std::cerr << "  Processed " << done << " way blocks..." << std::endl;
                            }
                        }
                    });
                }

                // Way reader: use the combined_reader (continuing from where nodes left off)
                {
                    // First, push the stashed way buffer
                    if (first_way_buf) {
                        std::unique_lock<std::mutex> lock(way_queue_mutex);
                        way_queue_cv.wait(lock, [&]{ return way_block_queue.size() < WAY_MAX_QUEUE; });
                        way_block_queue.push_back(std::move(first_way_buf));
                        way_queue_cv.notify_one();
                    }
                    // Continue reading remaining blocks from the same reader
                    while (auto buf = combined_reader.read()) {
                        {
                            std::unique_lock<std::mutex> lock(way_queue_mutex);
                            way_queue_cv.wait(lock, [&]{ return way_block_queue.size() < WAY_MAX_QUEUE; });
                            way_block_queue.push_back(std::move(buf));
                        }
                        way_queue_cv.notify_one();
                    }
                    combined_reader.close();
                    {
                        std::lock_guard<std::mutex> lock(way_queue_mutex);
                        way_reader_done = true;
                    }
                    way_queue_cv.notify_all();
                }

                for (auto& w : workers) w.join();
                std::cerr << "  Parallel way processing complete." << std::endl;

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

                    // Merge street ways
                    for (size_t i = 0; i < local.ways.size(); i++) {
                        auto h = local.ways[i];
                        h.node_offset += node_base;
                        h.name_id = data.string_pool.intern(local.way_strings[i]);
                        data.ways.push_back(h);
                    }
                    data.street_nodes.insert(data.street_nodes.end(),
                        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);
                    std::cerr << "  Assembling admin polygons in parallel ("
                              << data.collected_relations.size() << " relations, "
                              << data.way_geometries.size() << " way geometries)..." << std::endl;

                    // Thread-local admin results (to avoid locking add_admin_polygon)
                    struct AdminResult {
                        std::vector<std::pair<double,double>> vertices;
                        std::string name;
                        uint8_t admin_level;
                        std::string country_code;
                    };

                    std::vector<std::vector<AdminResult>> thread_admin_results(num_threads);
                    std::atomic<size_t> rel_idx{0};
                    std::atomic<uint64_t> assembled_count{0};

                    std::vector<std::thread> admin_workers;
                    for (unsigned int t = 0; t < num_threads; t++) {
                        admin_workers.emplace_back([&, t]() {
                            auto& local_results = thread_admin_results[t];
                            while (true) {
                                size_t i = rel_idx.fetch_add(1);
                                if (i >= data.collected_relations.size()) break;

                                const auto& rel = data.collected_relations[i];

                                // Skip level 2 border-line relations — these have names like
                                // "Deutschland - Österreich" (two countries separated by
                                // " - " or " — "). They are boundary lines, not country
                                // polygons, and produce self-intersecting geometry.
                                if (rel.admin_level == 2 && rel.country_code.empty() &&
                                    (rel.name.find(" - ") != std::string::npos ||
                                     rel.name.find(" \xe2\x80\x94 ") != std::string::npos)) { // " — " (em dash)
                                    assembled_count.fetch_add(1);
                                    continue;
                                }

                                // Skip relations with missing member ways —
                                // these cross the extract boundary and would produce
                                // incorrect partial polygons
                                bool has_missing = false;
                                for (const auto& [way_id, role] : rel.members) {
                                    if (data.way_geometries.find(way_id) == data.way_geometries.end()) {
                                        has_missing = true;
                                        break;
                                    }
                                }
                                if (has_missing) {
                                    assembled_count.fetch_add(1);
                                    continue;
                                }

                                auto rings = assemble_outer_rings(rel.members, data.way_geometries);
                                // If outer-only assembly failed, retry with all ways.
                                // Osmium ignores roles during assembly (check_roles=false) —
                                // inner ways sometimes form the boundary or bridge gaps.
                                // Filter retry results: discard rings with duplicate coords
                                // (figure-8 shapes from merging holes with outer boundary).
                                if (rings.empty()) {
                                    auto retry = assemble_outer_rings(rel.members, data.way_geometries, true);
                                    auto ck = [](double lat, double lng) -> int64_t {
                                        int32_t ilat = static_cast<int32_t>(lat * 1e7 + (lat >= 0 ? 0.5 : -0.5));
                                        int32_t ilng = static_cast<int32_t>(lng * 1e7 + (lng >= 0 ? 0.5 : -0.5));
                                        return (static_cast<int64_t>(ilat) << 32) | static_cast<uint32_t>(ilng);
                                    };
                                    for (auto& ring : retry) {
                                        if (!ring_has_duplicate_coords(ring, ck)) {
                                            rings.push_back(std::move(ring));
                                        }
                                    }
                                }

                                // Diagnostic: log relations with 0 rings produced
                                if (rings.empty() && !rel.members.empty()) {
                                    int total_ways = 0, found = 0, missing = 0;
                                    for (const auto& [way_id, role] : rel.members) {
                                        total_ways++;
                                        auto it = data.way_geometries.find(way_id);
                                        if (it != data.way_geometries.end() && !it->second.coords.empty()) {
                                            found++;
                                        } else {
                                            missing++;
                                        }
                                    }
                                    if (missing == 0 && found > 0) {
                                        std::string detail = "  DEBUG '" + rel.name + "': ways:";
                                        for (const auto& [way_id, role] : rel.members) {
                                            auto it = data.way_geometries.find(way_id);
                                            if (it == data.way_geometries.end()) continue;
                                            detail += " [w" + std::to_string(way_id) +
                                                      " first=" + std::to_string(it->second.first_node_id) +
                                                      " last=" + std::to_string(it->second.last_node_id) +
                                                      " n=" + std::to_string(it->second.coords.size()) + "]";
                                        }
                                        std::cerr << detail << std::endl;
                                    }
                                    std::cerr << "  WARN: relation '" << rel.name
                                              << "' (level " << (int)rel.admin_level
                                              << ", " << rel.members.size() << " members): "
                                              << "0 rings from " << total_ways << " ways ("
                                              << found << " found, " << missing << " missing)" << std::endl;
                                }

                                for (auto& ring : rings) {
                                    if (ring.size() >= 3) {
                                        AdminResult ar;
                                        ar.vertices = std::move(ring);
                                        ar.name = rel.name;
                                        ar.admin_level = rel.admin_level;
                                        ar.country_code = rel.country_code;
                                        local_results.push_back(std::move(ar));
                                    }
                                }

                                uint64_t done = assembled_count.fetch_add(1) + 1;
                                if (done % 10000 == 0) {
                                    std::cerr << "  Assembled " << done / 1000
                                              << "K admin relations..." << std::endl;
                                }
                            }
                        });
                    }
                    for (auto& w : admin_workers) w.join();

                    // Merge thread-local admin results into ParsedData (sequential, thread-safe)
                    uint64_t total_admin_rings = 0;
                    for (auto& local_results : thread_admin_results) {
                        for (auto& ar : local_results) {
                            const char* cc = ar.country_code.empty() ? nullptr : ar.country_code.c_str();
                            add_admin_polygon(data, ar.vertices, ar.name.c_str(),
                                              ar.admin_level, cc, &admin_pool);
                            total_admin_rings++;
                        }
                    }
                    std::cerr << "  Parallel admin assembly complete: "
                              << total_admin_rings << " polygon rings from "
                              << data.collected_relations.size() << " relations." << std::endl;

                    // Free collected data
                    data.collected_relations.clear();
                    data.collected_relations.shrink_to_fit();
                    data.way_geometries.clear();
                    std::unordered_map<int64_t, ParsedData::WayGeometry>().swap(data.way_geometries);
                }
            }
        }

        std::cerr << "Done reading:" << std::endl;
        std::cerr << "  " << data.ways.size() << " street ways" << std::endl;
        std::cerr << "  " << data.addr_points.size() << " address points" << std::endl;
        std::cerr << "  " << data.interp_ways.size() << " interpolation ways" << std::endl;
        std::cerr << "  " << data.admin_polygons.size() << " admin polygon rings" << std::endl;

        // Drain admin polygon thread pool (may still be processing)
        std::cerr << "Waiting for admin polygon S2 covering to complete..." << std::endl;
        auto admin_results = admin_pool.drain();
        for (auto& [cell_id, ids] : admin_results) {
            auto& target = data.cell_to_admin[cell_id];
            target.insert(target.end(), ids.begin(), ids.end());
        }
        std::cerr << "Admin polygon S2 covering complete (" << data.cell_to_admin.size() << " cells)." << std::endl;

        // --- Parallel S2 cell computation for ways and interpolation ---
        {
            log_phase("Admin assembly", _pt);
            std::cerr << "Computing S2 cells for ways with " << num_threads << " threads..." << std::endl;

            // Flat-array approach: threads emit (cell_id, item_id) pairs into
            // thread-local vectors, then we sort globally and group by cell_id.
            // This avoids hash maps entirely — sort is cache-friendly O(n log n).

            struct CellItem { uint64_t cell_id; uint32_t item_id; };

            auto _s2t = std::chrono::steady_clock::now();

            // Process streets: emit (cell_id, way_id) pairs
            std::cerr << "  Processing " << data.deferred_ways.size() << " street ways..." << std::endl;
            std::vector<std::vector<CellItem>> way_pairs(num_threads);
            {
                std::atomic<size_t> way_idx{0};
                std::vector<std::thread> threads;
                for (unsigned int t = 0; t < num_threads; t++) {
                    threads.emplace_back([&, t]() {
                        auto& local = way_pairs[t];
                        local.reserve(data.deferred_ways.size() / num_threads * 3);
                        std::vector<S2CellId> edge_cells;
                        std::vector<uint64_t> way_cells;
                        while (true) {
                            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});
                            }
                        }
                    });
                }


  2800→                for (auto& t : threads) t.join();
  2801→            }
  2802→            log_phase("  S2: street ways (parallel)", _s2t);
  2803→
  2804→            // Process interpolations: emit (cell_id, interp_id) pairs
  2805→            std::cerr << "  Processing " << data.deferred_interps.size() << " interpolation ways..." << std::endl;
  2806→            std::vector<std::vector<CellItem>> interp_pairs(num_threads);
  2807→            {
  2808→                std::atomic<size_t> interp_idx{0};
  2809→                std::vector<std::thread> threads;
  2810→                for (unsigned int t = 0; t < num_threads; t++) {
  2811→                    threads.emplace_back([&, t]() {
  2812→                        auto& local = interp_pairs[t];
  2813→                        std::vector<S2CellId> edge_cells;
  2814→                        std::vector<uint64_t> way_cells;
  2815→                        while (true) {
  2816→                            size_t i = interp_idx.fetch_add(1);
  2817→                            if (i >= data.deferred_interps.size()) break;
  2818→                            const auto& di = data.deferred_interps[i];
  2819→                            way_cells.clear();
  2820→                            for (uint8_t j = 0; j + 1 < di.node_count; j++) {
  2821→                                const auto& n1 = data.interp_nodes[di.node_offset + j];
  2822→                                const auto& n2 = data.interp_nodes[di.node_offset + j + 1];
  2823→                                cover_edge(n1.lat, n1.lng, n2.lat, n2.lng, edge_cells);
  2824→                                for (const auto& c : edge_cells) way_cells.push_back(c.id());
  2825→                            }
  2826→                            std::sort(way_cells.begin(), way_cells.end());
  2827→                            way_cells.erase(std::unique(way_cells.begin(), way_cells.end()), way_cells.end());
  2828→                            for (uint64_t cell_id : way_cells) {
  2829→                                local.push_back({cell_id, di.interp_id});
  2830→                            }
  2831→                        }
  2832→                    });
  2833→                }
  2834→                for (auto& t : threads) t.join();
  2835→            }
  2836→            log_phase("  S2: interp ways (parallel)", _s2t);
  2837→
  2838→            // Merge thread-local pairs into single vectors, sort, build cell maps
  2839→            std::cerr << "  Sorting and grouping cell pairs..." << std::endl;
  2840→            // Concatenate + sort helper for flat CellItem pairs
  2841→            // Parallel sort each thread's pairs, then k-way merge directly into
  2842→            // hash map + sorted output. No intermediate merged vector needed.
  2843→            auto parallel_sort_and_build = [](
  2844→                std::vector<std::vector<CellItem>>& thread_pairs,
  2845→                std::unordered_map<uint64_t, std::vector<uint32_t>>& cell_map,
  2846→                std::vector<CellItemPair>& sorted_out
  2847→            ) {
  2848→                // Step 1: Convert + sort each thread's data in parallel
  2849→                size_t total = 0;
  2850→                for (auto& v : thread_pairs) total += v.size();
  2851→
  2852→                std::vector<std::vector<CellItemPair>> chunks(thread_pairs.size());
  2853→                {
  2854→                    std::vector<std::thread> sort_threads;
  2855→                    for (size_t t = 0; t < thread_pairs.size(); t++) {
  2856→                        sort_threads.emplace_back([&, t]() {
  2857→                            auto& src = thread_pairs[t];
  2858→                            auto& dst = chunks[t];
  2859→                            dst.reserve(src.size());
  2860→                            for (auto& ci : src) dst.push_back({ci.cell_id, ci.item_id});
  2861→                            src.clear(); src.shrink_to_fit();
  2862→                            std::sort(dst.begin(), dst.end(), [](const CellItemPair& a, const CellItemPair& b) {
  2863→                                return a.cell_id < b.cell_id || (a.cell_id == b.cell_id && a.item_id < b.item_id);
  2864→                            });
  2865→                        });
  2866→                    }
  2867→                    for (auto& t : sort_threads) t.join();
  2868→                }
  2869→
  2870→                // Step 2: Concatenate sorted chunks and merge using parallel tree.
  2871→                // Each chunk is already sorted. Record run boundaries, concatenate,
  2872→                // then merge adjacent runs pairwise in parallel at each level.
  2873→                auto cmp = [](const CellItemPair& a, const CellItemPair& b) {
  2874→                    return a.cell_id < b.cell_id || (a.cell_id == b.cell_id && a.item_id < b.item_id);
  2875→                };
  2876→
  2877→                // Record run boundaries before concatenation
  2878→                std::vector<size_t> run_bounds = {0};
  2879→                sorted_out.clear();
  2880→                sorted_out.reserve(total);
  2881→                for (auto& chunk : chunks) {
  2882→                    sorted_out.insert(sorted_out.end(), chunk.begin(), chunk.end());
  2883→                    run_bounds.push_back(sorted_out.size());
  2884→                    chunk.clear(); chunk.shrink_to_fit();
  2885→                }
  2886→
  2887→                // Parallel tree merge: at each level, merge adjacent run pairs in parallel.
  2888→                // Level 0: merge runs (0,1), (2,3), ... → N/2 runs
  2889→                // Level 1: merge runs (01,23), (45,67), ... → N/4 runs
  2890→                // ... until one sorted run remains.
  2891→                while (run_bounds.size() > 2) {
  2892→                    std::vector<size_t> new_bounds = {0};
  2893→                    std::vector<std::thread> merge_threads;
  2894→                    for (size_t i = 0; i + 2 < run_bounds.size(); i += 2) {
  2895→                        size_t left = run_bounds[i];
  2896→                        size_t mid = run_bounds[i + 1];
  2897→                        size_t right = run_bounds[i + 2];
  2898→                        merge_threads.emplace_back([&, left, mid, right] {
  2899→                            std::inplace_merge(sorted_out.begin() + left,
  2900→                                               sorted_out.begin() + mid,
  2901→                                               sorted_out.begin() + right, cmp);
  2902→                        });
  2903→                        new_bounds.push_back(right);
  2904→                    }
  2905→                    // If odd number of runs, carry the last one forward
  2906→                    if (run_bounds.size() % 2 == 0) {
  2907→                        new_bounds.push_back(run_bounds.back());
  2908→                    }
  2909→                    for (auto& t : merge_threads) t.join();
  2910→                    run_bounds = std::move(new_bounds);
  2911→                }
  2912→
  2913→                // Skip building hash map — sorted_out is used directly for writing.
  2914→                // cell_map stays empty (only needed for cache/continent modes).
  2915→            };
  2916→
  2917→
  2918→            // Sort each thread's pairs in parallel, then k-way merge directly
  2919→            // into hash map + sorted output. No intermediate merged vector.
  2920→            auto f_ways = std::async(std::launch::async, [&] {
  2921→                parallel_sort_and_build(way_pairs, data.cell_to_ways, data.sorted_way_cells);
  2922→            });
  2923→            auto f_interps = std::async(std::launch::async, [&] {
  2924→                std::vector<CellItemPair> interp_sorted; // not needed for writing
  2925→                parallel_sort_and_build(interp_pairs, data.cell_to_interps, interp_sorted);
  2926→            });
  2927→            f_ways.get();
  2928→            f_interps.get();
  2929→            log_phase("  S2: sort + group into cell maps", _s2t);
  2930→
  2931→            // Free deferred work items
  2932→            data.deferred_ways.clear();
  2933→            data.deferred_ways.shrink_to_fit();
  2934→            data.deferred_interps.clear();
  2935→            data.deferred_interps.shrink_to_fit();
  2936→
  2937→            std::cerr << "S2 cell computation complete." << std::endl;
  2938→        }
  2939→
  2940→        // Resolve interpolation endpoints
  2941→        std::cerr << "Resolving interpolation endpoints..." << std::endl;
  2942→        resolve_interpolation_endpoints(data);
  2943→
  2944→        // Deduplicate + convert to sorted pairs for fast writing
  2945→        log_phase("S2 cell computation", _pt);
  2946→        std::cerr << "Deduplicating + sorting for write..." << std::endl;
  2947→        {
  2948→            // Convert addr hash map to sorted pairs.
  2949→            // Sort cell IDs only (30M unique), then build pairs in sorted order.
  2950→            // Much faster than sorting all 160M pairs.
  2951→            auto f2 = std::async(std::launch::async, [&] {
  2952→                // Extract and sort unique cell IDs
  2953→                std::vector<uint64_t> sorted_cells;
  2954→                sorted_cells.reserve(data.cell_to_addrs.size());
  2955→                size_t total_pairs = 0;
  2956→                for (auto& [cell_id, ids] : data.cell_to_addrs) {
  2957→                    sorted_cells.push_back(cell_id);
  2958→                    total_pairs += ids.size();
  2959→                }
  2960→                std::sort(sorted_cells.begin(), sorted_cells.end());
  2961→
  2962→                // Build sorted pairs by iterating sorted cell IDs
  2963→                std::vector<CellItemPair> pairs;
  2964→                pairs.reserve(total_pairs);
  2965→                for (uint64_t cell_id : sorted_cells) {
  2966→                    auto& ids = data.cell_to_addrs[cell_id];
  2967→                    std::sort(ids.begin(), ids.end()); // sort within cell
  2968→                    ids.erase(std::unique(ids.begin(), ids.end()), ids.end());
  2969→                    for (auto id : ids) pairs.push_back({cell_id, id});
  2970→                }
  2971→                data.sorted_addr_cells = std::move(pairs);
  2972→            });
  2973→            auto f4 = std::async(std::launch::async, [&]{ deduplicate(data.cell_to_admin); });
  2974→            f2.get(); f4.get();
  2975→        }
  2976→
  2977→        // Save cache if requested
  2978→        if (!save_cache_path.empty()) {
  2979→            serialize_cache(data, save_cache_path);
  2980→        }
  2981→    }
  2982→
  2983→    // --- Write index files ---
  2984→    log_phase("Deduplication", _pt);
  2985→    std::cerr << "Writing index files to " << output_dir << "..." << std::endl;
  2986→
  2987→    if (multi_output) {
  2988→        // Write all 3 index modes in parallel (they read shared data, write to separate dirs)
  2989→        auto wf1 = std::async(std::launch::async, [&]{ write_index(data, output_dir + "/full", IndexMode::Full); });
  2990→        auto wf2 = std::async(std::launch::async, [&]{ write_index(data, output_dir + "/no-addresses", IndexMode::NoAddresses); });
  2991→        auto wf3 = std::async(std::launch::async, [&]{ write_index(data, output_dir + "/admin-only", IndexMode::AdminOnly); });
  2992→        wf1.get(); wf2.get(); wf3.get();
  2993→    } else {
  2994→        write_index(data, output_dir, mode);
  2995→    }
  2996→
  2997→    if (generate_continents) {
  2998→        // Process continents in parallel (each filters independently from shared data)
  2999→        std::vector<std::future<void>> continent_futures;
  3000→        std::mutex cerr_mutex;
  3001→        for (const auto& continent : kContinents) {
  3002→            continent_futures.push_back(std::async(std::launch::async, [&, continent]() {
  3003→                {
  3004→                    std::lock_guard<std::mutex> lock(cerr_mutex);
  3005→                    std::cerr << "Filtering for continent: " << continent.name << "..." << std::endl;
  3006→                }
  3007→                auto subset = filter_by_bbox(data, continent);
  3008→                std::string base = output_dir + "/" + continent.name;
  3009→                ensure_dir(base);
  3010→                if (multi_output) {
  3011→                    // Write modes in parallel within each continent
  3012→                    auto cf1 = std::async(std::launch::async, [&]{ write_index(subset, base + "/full", IndexMode::Full); });
  3013→                    auto cf2 = std::async(std::launch::async, [&]{ write_index(subset, base + "/no-addresses", IndexMode::NoAddresses); });
  3014→                    auto cf3 = std::async(std::launch::async, [&]{ write_index(subset, base + "/admin-only", IndexMode::AdminOnly); });
  3015→                    cf1.get(); cf2.get(); cf3.get();
  3016→                } else {
  3017→                    write_index(subset, base, mode);
  3018→                }
  3019→                {
  3020→                    std::lock_guard<std::mutex> lock(cerr_mutex);
  3021→                    std::cerr << "Done continent: " << continent.name << std::endl;
  3022→                }
  3023→            }));
  3024→        }
  3025→        for (auto& f : continent_futures) {
  3026→            f.get();
  3027→        }
  3028→    }
  3029→
  3030→    log_phase("Index file writing", _pt);
  3031→    std::cerr << "Done." << std::endl;
  3032→    return 0;
  3033→}
  3034→

                for (auto& t : threads) t.join();
            }
            log_phase("  S2: street ways (parallel)", _s2t);

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

            // 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, [&] {
                std::vector<CellItemPair> interp_sorted; // not needed for writing
                parallel_sort_and_build(interp_pairs, data.cell_to_interps, interp_sorted);
            });
            f_ways.get();
            f_interps.get();
            log_phase("  S2: sort + group into cell maps", _s2t);

            // 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);
        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, [&] {
                // Extract and sort unique cell IDs
                std::vector<uint64_t> sorted_cells;
                sorted_cells.reserve(data.cell_to_addrs.size());
                size_t total_pairs = 0;
                for (auto& [cell_id, ids] : data.cell_to_addrs) {
                    sorted_cells.push_back(cell_id);
                    total_pairs += ids.size();
                }
                std::sort(sorted_cells.begin(), sorted_cells.end());

                // Build sorted pairs by iterating sorted cell IDs
                std::vector<CellItemPair> pairs;
                pairs.reserve(total_pairs);
                for (uint64_t cell_id : sorted_cells) {
                    auto& ids = data.cell_to_addrs[cell_id];
                    std::sort(ids.begin(), ids.end()); // sort within cell
                    ids.erase(std::unique(ids.begin(), ids.end()), ids.end());
                    for (auto id : ids) pairs.push_back({cell_id, id});
                }
                data.sorted_addr_cells = std::move(pairs);
            });
            auto f4 = std::async(std::launch::async, [&]{ deduplicate(data.cell_to_admin); });
            f2.get(); f4.get();
        }

        // Save cache if requested
        if (!save_cache_path.empty()) {
            serialize_cache(data, save_cache_path);
        }
    }

    // --- Write index files ---
    log_phase("Deduplication", _pt);
    std::cerr << "Writing index files to " << output_dir << "..." << std::endl;

    if (multi_output) {
        // Write all 3 index modes in parallel (they read shared data, write to separate dirs)
        auto wf1 = std::async(std::launch::async, [&]{ write_index(data, output_dir + "/full", IndexMode::Full); });
        auto wf2 = std::async(std::launch::async, [&]{ write_index(data, output_dir + "/no-addresses", IndexMode::NoAddresses); });
        auto wf3 = std::async(std::launch::async, [&]{ write_index(data, output_dir + "/admin-only", IndexMode::AdminOnly); });
        wf1.get(); wf2.get(); wf3.get();
    } else {
        write_index(data, output_dir, mode);
    }

    if (generate_continents) {
        // Process continents in parallel (each filters independently from shared data)
        std::vector<std::future<void>> continent_futures;
        std::mutex cerr_mutex;
        for (const auto& continent : kContinents) {
            continent_futures.push_back(std::async(std::launch::async, [&, continent]() {
                {
                    std::lock_guard<std::mutex> lock(cerr_mutex);
                    std::cerr << "Filtering for continent: " << continent.name << "..." << std::endl;
                }
                auto subset = filter_by_bbox(data, continent);
                std::string base = output_dir + "/" + continent.name;
                ensure_dir(base);
                if (multi_output) {
                    // Write modes in parallel within each continent
                    auto cf1 = std::async(std::launch::async, [&]{ write_index(subset, base + "/full", IndexMode::Full); });
                    auto cf2 = std::async(std::launch::async, [&]{ write_index(subset, base + "/no-addresses", IndexMode::NoAddresses); });
                    auto cf3 = std::async(std::launch::async, [&]{ write_index(subset, base + "/admin-only", IndexMode::AdminOnly); });
                    cf1.get(); cf2.get(); cf3.get();
                } else {
                    write_index(subset, base, mode);
                }
                {
                    std::lock_guard<std::mutex> lock(cerr_mutex);
                    std::cerr << "Done continent: " << continent.name << std::endl;
                }
            }));
        }
        for (auto& f : continent_futures) {
            f.get();
        }
    }

    log_phase("Index file writing", _pt);
    std::cerr << "Done." << std::endl;
    return 0;
}


Now I have the full picture. Let me start implementing the refactoring plan — beginning with extracting utility functions into headers, adding more unit tests, and fixing the precision bug.


ToolSearch

Question: select:TaskCreate,TaskUpdate


Question: select:TaskCreate,TaskUpdate


TaskCreate

Extract utility headers from monolithic build_index.cpp

Extract reusable utility functions into separate headers:
1. geometry.h - coord_key, polygon_area, segments_intersect, ring_has_self_intersection, ring_has_duplicate_coords, dp_simplify, simplify_polygon
2. types.h - Binary format structs (WayHeader, AddrPoint, InterpWay, AdminPolygon, NodeCoord, CellItemPair), constants (INTERIOR_FLAG, ID_MASK, NO_DATA)
3. string_pool.h - StringPool class
These should be header-only to keep the build simple (single CMake target).


Task #1 created successfully: Extract utility headers from monolithic build_index.cpp


Add comprehensive unit tests for extracted utilities

Expand test_ring_assembly.cpp with tests for:
- polygon_area (triangles, degenerate, large polygons)
- segments_intersect (parallel, collinear, endpoint touching, T-junction)
- ring_has_self_intersection (large ring sweep-line path, threshold=32 boundary)
- simplify_polygon (already small, binary search convergence)
- parse_house_number (leading digits, alpha suffix, empty, null)
- is_included_highway (all excluded types, included types)
- StringPool (intern, dedup, empty string)
- coord_key (precision edge cases, max values, equator/prime meridian)
- ring_has_duplicate_coords (no dups, adjacent dups)
- DouglasPeucker edge cases


Task #2 created successfully: Add comprehensive unit tests for extracted utilities


Fix precision bug in resolve_interpolation_endpoints

resolve_interpolation_endpoints uses `* 100000` for coordinate keys while the rest of the code uses `* 1e7`. This means interpolation endpoint matching has ~100x less precision than coord_key, potentially missing matches for nearby but distinct addresses. Fix to use consistent precision.


Task #3 created successfully: Fix precision bug in resolve_interpolation_endpoints


Clean up dead code and extract magic numbers

- Remove unnecessary try/catch blocks in way processing lambda (index.get never throws)
- Extract magic numbers: 500 (max polygon vertices), 64 (max queue size), 255 (max node count), 65535 (max vertex count), 200 (max S2 cells), 100000/500000 (backtracking budgets), 30 (max sub-ways for retry)
- Remove duplicate coord_key lambda in admin assembly (line 2654) - use a shared function


Task #4 created successfully: Clean up dead code and extract magic numbers


Commit and backup to node 3

Commit all refactoring changes and rsync to node 3 backup.


Task #5 created successfully: Commit and backup to node 3

Commit and backup to node 3


Updated task #1 status


Bash

List source directory


Now I'll create the three header files. Starting with `types.h`:


Write

#pragma once

#include <cstdint>
#include <vector>

// --- Binary format structs (must match server's index reader) ---

struct WayHeader {
    uint32_t node_offset;
    uint8_t node_count;
    uint32_t name_id;
};

struct AddrPoint {
    float lat;
    float lng;
    uint32_t housenumber_id;
    uint32_t street_id;
};

struct InterpWay {
    uint32_t node_offset;
    uint8_t node_count;
    uint32_t street_id;
    uint32_t start_number;
    uint32_t end_number;
    uint8_t interpolation;
};

struct AdminPolygon {
    uint32_t vertex_offset;
    uint16_t vertex_count;
    uint32_t name_id;
    uint8_t admin_level;
    float area;
    uint16_t country_code;
};

struct NodeCoord {
    float lat;
    float lng;
};

// Sorted cell-item pair for direct entry writing (avoids hash map)
struct CellItemPair {
    uint64_t cell_id;
    uint32_t item_id;
};

// --- Constants ---

static const uint32_t INTERIOR_FLAG = 0x80000000u;
static const uint32_t ID_MASK       = 0x7FFFFFFFu;
static const uint32_t NO_DATA       = 0xFFFFFFFFu;

// --- Index mode ---

enum class IndexMode { Full, NoAddresses, AdminOnly };

// --- Deferred S2 work items ---

struct DeferredWay {
    uint32_t way_id;
    uint32_t node_offset;
    uint8_t node_count;
};

struct DeferredInterp {
    uint32_t interp_id;
    uint32_t node_offset;
    uint8_t node_count;
};

// Collected relation data for parallel admin assembly
struct CollectedRelation {
    int64_t id;
    uint8_t admin_level;
    std::string name;
    std::string country_code;
    bool is_postal;
    std::vector<std::pair<int64_t, std::string>> members; // (way_id, role)
};


File created successfully at: /root/traccar-geocoder/builder/src/types.h

#pragma once

#include <cstdint>
#include <vector>

// --- Binary format structs (must match server's index reader) ---

struct WayHeader {
    uint32_t node_offset;
    uint8_t node_count;
    uint32_t name_id;
};

struct AddrPoint {
    float lat;
    float lng;
    uint32_t housenumber_id;
    uint32_t street_id;
};

struct InterpWay {
    uint32_t node_offset;
    uint8_t node_count;
    uint32_t street_id;
    uint32_t start_number;
    uint32_t end_number;
    uint8_t interpolation;
};

struct AdminPolygon {
    uint32_t vertex_offset;
    uint16_t vertex_count;
    uint32_t name_id;
    uint8_t admin_level;
    float area;
    uint16_t country_code;
};

struct NodeCoord {
    float lat;
    float lng;
};

// Sorted cell-item pair for direct entry writing (avoids hash map)
struct CellItemPair {
    uint64_t cell_id;
    uint32_t item_id;
};

// --- Constants ---

static const uint32_t INTERIOR_FLAG = 0x80000000u;
static const uint32_t ID_MASK       = 0x7FFFFFFFu;
static const uint32_t NO_DATA       = 0xFFFFFFFFu;

// --- Index mode ---

enum class IndexMode { Full, NoAddresses, AdminOnly };

// --- Deferred S2 work items ---

struct DeferredWay {
    uint32_t way_id;
    uint32_t node_offset;
    uint8_t node_count;
};

struct DeferredInterp {
    uint32_t interp_id;
    uint32_t node_offset;
    uint8_t node_count;
};

// Collected relation data for parallel admin assembly
struct CollectedRelation {
    int64_t id;
    uint8_t admin_level;
    std::string name;
    std::string country_code;
    bool is_postal;
    std::vector<std::pair<int64_t, std::string>> members; // (way_id, role)
};


#pragma once

#include <cstdint>
#include <string>
#include <unordered_map>
#include <vector>

class StringPool {
public:
    uint32_t intern(const std::string& s) {
        auto it = index_.find(s);
        if (it != index_.end()) {
            return it->second;
        }
        uint32_t offset = static_cast<uint32_t>(data_.size());
        index_[s] = offset;
        data_.insert(data_.end(), s.begin(), s.end());
        data_.push_back('\0');
        return offset;
    }

    const std::vector<char>& data() const { return data_; }
    std::vector<char>& mutable_data() { return data_; }

private:
    std::unordered_map<std::string, uint32_t> index_;
    std::vector<char> data_;
};


File created successfully at: /root/traccar-geocoder/builder/src/string_pool.h

#pragma once

#include <cstdint>
#include <string>
#include <unordered_map>
#include <vector>

class StringPool {
public:
    uint32_t intern(const std::string& s) {
        auto it = index_.find(s);
        if (it != index_.end()) {
            return it->second;
        }
        uint32_t offset = static_cast<uint32_t>(data_.size());
        index_[s] = offset;
        data_.insert(data_.end(), s.begin(), s.end());
        data_.push_back('\0');
        return offset;
    }

    const std::vector<char>& data() const { return data_; }
    std::vector<char>& mutable_data() { return data_; }

private:
    std::unordered_map<std::string, uint32_t> index_;
    std::vector<char> data_;
};


#pragma once

#include <algorithm>
#include <cmath>
#include <cstdint>
#include <functional>
#include <unordered_set>
#include <vector>

// Coordinate key matching osmium's Location (int32_t nanodegrees).
// Two points at the same geographic location always produce the same key,
// regardless of their OSM node IDs (handles split/replaced nodes).
inline int64_t coord_key(double lat, double lng) {
    int32_t ilat = static_cast<int32_t>(lat * 1e7 + (lat >= 0 ? 0.5 : -0.5));
    int32_t ilng = static_cast<int32_t>(lng * 1e7 + (lng >= 0 ? 0.5 : -0.5));
    return (static_cast<int64_t>(ilat) << 32) | static_cast<uint32_t>(ilng);
}

// Approximate polygon area in square degrees (shoelace formula)
inline float polygon_area(const std::vector<std::pair<double,double>>& vertices) {
    double area = 0;
    size_t n = vertices.size();
    for (size_t i = 0; i < n; i++) {
        size_t j = (i + 1) % n;
        area += vertices[i].first * vertices[j].second;
        area -= vertices[j].first * vertices[i].second;
    }
    return static_cast<float>(std::fabs(area) / 2.0);
}

// Check if segment (a1,a2) crosses segment (b1,b2) using cross product orientation
inline bool segments_intersect(double ax1, double ay1, double ax2, double ay2,
                               double bx1, double by1, double bx2, double by2) {
    auto cross = [](double ox, double oy, double ax, double ay, double bx, double by) -> double {
        return (ax - ox) * (by - oy) - (ay - oy) * (bx - ox);
    };
    double d1 = cross(bx1, by1, bx2, by2, ax1, ay1);
    double d2 = cross(bx1, by1, bx2, by2, ax2, ay2);
    double d3 = cross(ax1, ay1, ax2, ay2, bx1, by1);
    double d4 = cross(ax1, ay1, ax2, ay2, bx2, by2);
    if (((d1 > 0 && d2 < 0) || (d1 < 0 && d2 > 0)) &&
        ((d3 > 0 && d4 < 0) || (d3 < 0 && d4 > 0))) {
        return true;
    }
    return false;
}

// Sweep-line inspired self-intersection check for rings.
// Brute force for small rings (<=32 segments), sweep-line pruning for large ones.
inline bool ring_has_self_intersection(const std::vector<std::pair<double,double>>& ring) {
    static constexpr size_t BRUTE_FORCE_THRESHOLD = 32;
    size_t n = ring.size();
    if (n < 4) return false;

    if (n <= BRUTE_FORCE_THRESHOLD) {
        for (size_t i = 0; i + 1 < n; i++) {
            for (size_t j = i + 2; j + 1 < n; j++) {
                if (j == i + 1 || (i == 0 && j == n - 2)) continue;
                if (segments_intersect(ring[i].first, ring[i].second,
                                       ring[i+1].first, ring[i+1].second,
                                       ring[j].first, ring[j].second,
                                       ring[j+1].first, ring[j+1].second))
                    return true;
            }
        }
        return false;
    }

    // For larger rings, sort segments by min-x and prune non-overlapping
    struct Seg { double min_x, max_x; size_t idx; };
    std::vector<Seg> segs(n - 1);
    for (size_t i = 0; i + 1 < n; i++) {
        double x1 = ring[i].second, x2 = ring[i+1].second; // use lng as x
        segs[i] = {std::min(x1,x2), std::max(x1,x2), i};
    }
    std::sort(segs.begin(), segs.end(), [](const Seg& a, const Seg& b) {
        return a.min_x < b.min_x;
    });

    for (size_t a = 0; a + 1 < segs.size(); a++) {
        for (size_t b = a + 1; b < segs.size(); b++) {
            if (segs[b].min_x > segs[a].max_x) break;
            size_t i = segs[a].idx, j = segs[b].idx;
            if (j == i + 1 || i == j + 1) continue;
            if ((i == 0 && j == n - 2) || (j == 0 && i == n - 2)) continue;
            if (segments_intersect(ring[i].first, ring[i].second,
                                   ring[i+1].first, ring[i+1].second,
                                   ring[j].first, ring[j].second,
                                   ring[j+1].first, ring[j+1].second))
                return true;
        }
    }
    return false;
}

// Check if a ring has duplicate coordinates (spikes/figure-8 from merged holes).
// A valid simple polygon visits each coordinate exactly once (except closing point).
inline bool ring_has_duplicate_coords(const std::vector<std::pair<double,double>>& ring) {
    std::unordered_set<int64_t> seen;
    for (size_t i = 0; i + 1 < ring.size(); i++) {
        int64_t k = coord_key(ring[i].first, ring[i].second);
        if (!seen.insert(k).second) return true;
    }
    return false;
}

// Douglas-Peucker line simplification
inline void dp_simplify(const std::vector<std::pair<double,double>>& pts,
                        size_t start, size_t end, double epsilon,
                        std::vector<bool>& keep) {
    if (end <= start + 1) return;

    double max_dist = 0;
    size_t max_idx = start;

    double ax = pts[start].first, ay = pts[start].second;
    double bx = pts[end].first, by = pts[end].second;
    double dx = bx - ax, dy = by - ay;
    double len_sq = dx * dx + dy * dy;

    for (size_t i = start + 1; i < end; i++) {
        double px = pts[i].first - ax, py = pts[i].second - ay;
        double dist;
        if (len_sq == 0) {
            dist = std::sqrt(px * px + py * py);
        } else {
            double t = std::max(0.0, std::min(1.0, (px * dx + py * dy) / len_sq));
            double proj_x = t * dx - px, proj_y = t * dy - py;
            dist = std::sqrt(proj_x * proj_x + proj_y * proj_y);
        }
        if (dist > max_dist) {
            max_dist = dist;
            max_idx = i;
        }
    }

    if (max_dist > epsilon) {
        keep[max_idx] = true;
        dp_simplify(pts, start, max_idx, epsilon, keep);
        dp_simplify(pts, max_idx, end, epsilon, keep);
    }
}

static constexpr size_t MAX_POLYGON_VERTICES = 500;

inline std::vector<std::pair<double,double>> simplify_polygon(
    const std::vector<std::pair<double,double>>& pts, size_t max_vertices = MAX_POLYGON_VERTICES) {
    if (pts.size() <= max_vertices) return pts;

    double lo = 0, hi = 1.0;
    std::vector<std::pair<double,double>> result;

    for (int iter = 0; iter < 20; iter++) {
        double epsilon = (lo + hi) / 2;
        std::vector<bool> keep(pts.size(), false);
        keep[0] = true;
        keep[pts.size() - 1] = true;
        dp_simplify(pts, 0, pts.size() - 1, epsilon, keep);

        size_t count = 0;
        for (bool k : keep) if (k) count++;

        if (count > max_vertices) {
            lo = epsilon;
        } else {
            hi = epsilon;
        }
    }

    std::vector<bool> keep(pts.size(), false);
    keep[0] = true;
    keep[pts.size() - 1] = true;
    dp_simplify(pts, 0, pts.size() - 1, hi, keep);

    result.clear();
    for (size_t i = 0; i < pts.size(); i++) {
        if (keep[i]) result.push_back(pts[i]);
    }
    return result;
}

// Parse leading digits from a house number string
inline uint32_t parse_house_number(const char* s) {
    if (!s) return 0;
    uint32_t n = 0;
    while (*s >= '0' && *s <= '9') {
        n = n * 10 + (*s - '0');
        s++;
    }
    return n;
}

// Highway types excluded from street indexing
inline bool is_included_highway(const char* value) {
    static const char* kExcluded[] = {
        "footway", "path", "track", "steps", "cycleway",
        "service", "pedestrian", "bridleway", "construction"
    };
    for (const char* excluded : kExcluded) {
        if (std::strcmp(excluded, value) == 0) return false;
    }
    return true;
}


File created successfully at: /root/traccar-geocoder/builder/src/geometry.h

#pragma once

#include <algorithm>
#include <cmath>
#include <cstdint>
#include <functional>
#include <unordered_set>
#include <vector>

// Coordinate key matching osmium's Location (int32_t nanodegrees).
// Two points at the same geographic location always produce the same key,
// regardless of their OSM node IDs (handles split/replaced nodes).
inline int64_t coord_key(double lat, double lng) {
    int32_t ilat = static_cast<int32_t>(lat * 1e7 + (lat >= 0 ? 0.5 : -0.5));
    int32_t ilng = static_cast<int32_t>(lng * 1e7 + (lng >= 0 ? 0.5 : -0.5));
    return (static_cast<int64_t>(ilat) << 32) | static_cast<uint32_t>(ilng);
}

// Approximate polygon area in square degrees (shoelace formula)
inline float polygon_area(const std::vector<std::pair<double,double>>& vertices) {
    double area = 0;
    size_t n = vertices.size();
    for (size_t i = 0; i < n; i++) {
        size_t j = (i + 1) % n;
        area += vertices[i].first * vertices[j].second;
        area -= vertices[j].first * vertices[i].second;
    }
    return static_cast<float>(std::fabs(area) / 2.0);
}

// Check if segment (a1,a2) crosses segment (b1,b2) using cross product orientation
inline bool segments_intersect(double ax1, double ay1, double ax2, double ay2,
                               double bx1, double by1, double bx2, double by2) {
    auto cross = [](double ox, double oy, double ax, double ay, double bx, double by) -> double {
        return (ax - ox) * (by - oy) - (ay - oy) * (bx - ox);
    };
    double d1 = cross(bx1, by1, bx2, by2, ax1, ay1);
    double d2 = cross(bx1, by1, bx2, by2, ax2, ay2);
    double d3 = cross(ax1, ay1, ax2, ay2, bx1, by1);
    double d4 = cross(ax1, ay1, ax2, ay2, bx2, by2);
    if (((d1 > 0 && d2 < 0) || (d1 < 0 && d2 > 0)) &&
        ((d3 > 0 && d4 < 0) || (d3 < 0 && d4 > 0))) {
        return true;
    }
    return false;
}

// Sweep-line inspired self-intersection check for rings.
// Brute force for small rings (<=32 segments), sweep-line pruning for large ones.
inline bool ring_has_self_intersection(const std::vector<std::pair<double,double>>& ring) {
    static constexpr size_t BRUTE_FORCE_THRESHOLD = 32;
    size_t n = ring.size();
    if (n < 4) return false;

    if (n <= BRUTE_FORCE_THRESHOLD) {
        for (size_t i = 0; i + 1 < n; i++) {
            for (size_t j = i + 2; j + 1 < n; j++) {
                if (j == i + 1 || (i == 0 && j == n - 2)) continue;
                if (segments_intersect(ring[i].first, ring[i].second,
                                       ring[i+1].first, ring[i+1].second,
                                       ring[j].first, ring[j].second,
                                       ring[j+1].first, ring[j+1].second))
                    return true;
            }
        }
        return false;
    }

    // For larger rings, sort segments by min-x and prune non-overlapping
    struct Seg { double min_x, max_x; size_t idx; };
    std::vector<Seg> segs(n - 1);
    for (size_t i = 0; i + 1 < n; i++) {
        double x1 = ring[i].second, x2 = ring[i+1].second; // use lng as x
        segs[i] = {std::min(x1,x2), std::max(x1,x2), i};
    }
    std::sort(segs.begin(), segs.end(), [](const Seg& a, const Seg& b) {
        return a.min_x < b.min_x;
    });

    for (size_t a = 0; a + 1 < segs.size(); a++) {
        for (size_t b = a + 1; b < segs.size(); b++) {
            if (segs[b].min_x > segs[a].max_x) break;
            size_t i = segs[a].idx, j = segs[b].idx;
            if (j == i + 1 || i == j + 1) continue;
            if ((i == 0 && j == n - 2) || (j == 0 && i == n - 2)) continue;
            if (segments_intersect(ring[i].first, ring[i].second,
                                   ring[i+1].first, ring[i+1].second,
                                   ring[j].first, ring[j].second,
                                   ring[j+1].first, ring[j+1].second))
                return true;
        }
    }
    return false;
}

// Check if a ring has duplicate coordinates (spikes/figure-8 from merged holes).
// A valid simple polygon visits each coordinate exactly once (except closing point).
inline bool ring_has_duplicate_coords(const std::vector<std::pair<double,double>>& ring) {
    std::unordered_set<int64_t> seen;
    for (size_t i = 0; i + 1 < ring.size(); i++) {
        int64_t k = coord_key(ring[i].first, ring[i].second);
        if (!seen.insert(k).second) return true;
    }
    return false;
}

// Douglas-Peucker line simplification
inline void dp_simplify(const std::vector<std::pair<double,double>>& pts,
                        size_t start, size_t end, double epsilon,
                        std::vector<bool>& keep) {
    if (end <= start + 1) return;

    double max_dist = 0;
    size_t max_idx = start;

    double ax = pts[start].first, ay = pts[start].second;
    double bx = pts[end].first, by = pts[end].second;
    double dx = bx - ax, dy = by - ay;
    double len_sq = dx * dx + dy * dy;

    for (size_t i = start + 1; i < end; i++) {
        double px = pts[i].first - ax, py = pts[i].second - ay;
        double dist;
        if (len_sq == 0) {
            dist = std::sqrt(px * px + py * py);
        } else {
            double t = std::max(0.0, std::min(1.0, (px * dx + py * dy) / len_sq));
            double proj_x = t * dx - px, proj_y = t * dy - py;
            dist = std::sqrt(proj_x * proj_x + proj_y * proj_y);
        }
        if (dist > max_dist) {
            max_dist = dist;
            max_idx = i;
        }
    }

    if (max_dist > epsilon) {
        keep[max_idx] = true;
        dp_simplify(pts, start, max_idx, epsilon, keep);
        dp_simplify(pts, max_idx, end, epsilon, keep);
    }
}

static constexpr size_t MAX_POLYGON_VERTICES = 500;

inline std::vector<std::pair<double,double>> simplify_polygon(
    const std::vector<std::pair<double,double>>& pts, size_t max_vertices = MAX_POLYGON_VERTICES) {
    if (pts.size() <= max_vertices) return pts;

    double lo = 0, hi = 1.0;
    std::vector<std::pair<double,double>> result;

    for (int iter = 0; iter < 20; iter++) {
        double epsilon = (lo + hi) / 2;
        std::vector<bool> keep(pts.size(), false);
        keep[0] = true;
        keep[pts.size() - 1] = true;
        dp_simplify(pts, 0, pts.size() - 1, epsilon, keep);

        size_t count = 0;
        for (bool k : keep) if (k) count++;

        if (count > max_vertices) {
            lo = epsilon;
        } else {
            hi = epsilon;
        }
    }

    std::vector<bool> keep(pts.size(), false);
    keep[0] = true;
    keep[pts.size() - 1] = true;
    dp_simplify(pts, 0, pts.size() - 1, hi, keep);

    result.clear();
    for (size_t i = 0; i < pts.size(); i++) {
        if (keep[i]) result.push_back(pts[i]);
    }
    return result;
}

// Parse leading digits from a house number string
inline uint32_t parse_house_number(const char* s) {
    if (!s) return 0;
    uint32_t n = 0;
    while (*s >= '0' && *s <= '9') {
        n = n * 10 + (*s - '0');
        s++;
    }
    return n;
}

// Highway types excluded from street indexing
inline bool is_included_highway(const char* value) {
    static const char* kExcluded[] = {
        "footway", "path", "track", "steps", "cycleway",
        "service", "pedestrian", "bridleway", "construction"
    };
    for (const char* excluded : kExcluded) {
        if (std::strcmp(excluded, value) == 0) return false;
    }
    return true;
}


Now update `build_index.cpp` to use the extracted headers instead of inline definitions: