Now let me replace the entire function body. This is a big change — I'll write the complete osmium-style assembler.


   525→
   526→static std::vector<std::vector<std::pair<double,double>>> assemble_outer_rings(
   527→    const std::vector<std::pair<int64_t, std::string>>& members,
   528→    const std::unordered_map<int64_t, ParsedData::WayGeometry>& way_geoms,
   529→    bool include_all_roles = false)

static std::vector<std::vector<std::pair<double,double>>> assemble_outer_rings(
    const std::vector<std::pair<int64_t, std::string>>& members,
    const std::unordered_map<int64_t, ParsedData::WayGeometry>& way_geoms,
    bool include_all_roles = false)


   530→{
   531→    // Coordinate-based ring assembly: split ways at shared internal nodes,
   532→    // then stitch sub-ways at endpoints with backtracking.
   533→    // This matches osmium's segment-level approach.
   534→
   535→    // Coordinate key matching osmium's Location (int32_t nanodegrees)
   536→    auto coord_key = [](double lat, double lng) -> int64_t {
   537→        int32_t ilat = static_cast<int32_t>(lat * 1e7 + (lat >= 0 ? 0.5 : -0.5));
   538→        int32_t ilng = static_cast<int32_t>(lng * 1e7 + (lng >= 0 ? 0.5 : -0.5));
   539→        return (static_cast<int64_t>(ilat) << 32) | static_cast<uint32_t>(ilng);
   540→    };
   541→
   542→    // Collect coordinates from member ways.
   543→    // First pass uses outer/empty-role ways only. If that produces 0 rings,
   544→    // retry with ALL ways (matching osmium's check_roles=false fallback) —
   545→    // inner ways sometimes bridge gaps between outer ways.
   546→    std::unordered_map<int64_t, int> coord_count;
   547→    std::vector<const std::vector<std::pair<double,double>>*> way_coords;
   548→    for (const auto& [way_id, role] : members) {
   549→        if (!include_all_roles && role != "outer" && !role.empty()) continue;
   550→        auto it = way_geoms.find(way_id);
   551→        if (it == way_geoms.end() || it->second.coords.empty()) continue;
   552→        way_coords.push_back(&it->second.coords);
   553→        for (const auto& [lat, lng] : it->second.coords) {
   554→            coord_count[coord_key(lat, lng)]++;
   555→        }
   556→    }
   557→
   558→    // A coordinate is a "split point" if it appears in 2+ ways' node lists.
   559→    // Endpoints always count, internal nodes that appear in multiple ways need splitting.
   560→    // Also, all way endpoints are natural split points.
   561→    std::unordered_set<int64_t> split_points;
   562→    for (auto* coords : way_coords) {
   563→        split_points.insert(coord_key(coords->front().first, coords->front().second));
   564→        split_points.insert(coord_key(coords->back().first, coords->back().second));
   565→    }
   566→    for (auto& [ck, cnt] : coord_count) {
   567→        if (cnt > 1) split_points.insert(ck);
   568→    }
   569→
   570→    // Split each way at split points to create sub-ways
   571→    struct SubWay {
   572→        std::vector<std::pair<double,double>> coords;
   573→    };
   574→    std::vector<SubWay> sub_ways;
   575→    for (auto* coords : way_coords) {
   576→        size_t seg_start = 0;
   577→        for (size_t i = 1; i < coords->size(); i++) {
   578→            int64_t ck = coord_key((*coords)[i].first, (*coords)[i].second);
   579→            if (split_points.count(ck) && i > seg_start) {
   580→                // Create sub-way from seg_start to i (inclusive)
   581→                SubWay sw;
   582→                sw.coords.assign(coords->begin() + seg_start, coords->begin() + i + 1);
   583→                if (sw.coords.size() >= 2) {
   584→                    sub_ways.push_back(std::move(sw));
   585→                }
   586→                seg_start = i;
   587→            }
   588→        }
   589→        // Handle remaining segment (if not already added)
   590→        if (seg_start < coords->size() - 1) {
   591→            SubWay sw;
   592→            sw.coords.assign(coords->begin() + seg_start, coords->end());
   593→            if (sw.coords.size() >= 2) {
   594→                sub_ways.push_back(std::move(sw));
   595→            }
   596→        }
   597→    }
   598→
   599→    // Build adjacency by sub-way coordinate endpoints
   600→    struct WayRef {
   601→        int64_t way_id;
   602→        const ParsedData::WayGeometry* geom; // unused for sub-ways
   603→    };
   604→    // We'll adapt the BacktrackState to work with sub_ways directly
   605→    std::unordered_map<int64_t, std::vector<std::pair<size_t, bool>>> coord_adj;
   606→    for (size_t i = 0; i < sub_ways.size(); i++) {
   607→        auto& first = sub_ways[i].coords.front();
   608→        auto& last = sub_ways[i].coords.back();
   609→        coord_adj[coord_key(first.first, first.second)].push_back({i, false});
   610→        coord_adj[coord_key(last.first, last.second)].push_back({i, true});
   611→    }
   612→
   613→    std::vector<bool> used(sub_ways.size(), false);
   614→    std::vector<std::vector<std::pair<double,double>>> rings;
   615→
   616→    // Ring closure: greedy first (fast), then full backtracking retry from
   617→    // scratch if greedy left unused sub-ways. The second pass gets clean
   618→    // used flags so backtracking makes optimal branch choices.
   619→
   620→    // Backtracking with depth limit (for fallback only)
   621→    struct BacktrackState {
   622→        const std::vector<SubWay>& sways;
   623→        const std::unordered_map<int64_t, std::vector<std::pair<size_t, bool>>>& adj;
   624→        std::vector<bool>& used;
   625→        decltype(coord_key)& key_fn;
   626→        int calls;
   627→
   628→        bool try_close(int64_t first_key, int64_t last_key,
   629→                       std::vector<std::pair<size_t, bool>>& path, int depth) {
   630→            if (++calls > 100000) return false; // limit total work, not just depth
   631→            if (!path.empty() && first_key == last_key) return true;
   632→            if (depth > 200) return false;
   633→
   634→            auto it = adj.find(last_key);
   635→            if (it == adj.end()) return false;
   636→
   637→            for (const auto& [wi, is_last] : it->second) {
   638→                if (used[wi]) continue;
   639→                auto& endpoint = is_last ? sways[wi].coords.front() : sways[wi].coords.back();
   640→                int64_t new_key = key_fn(endpoint.first, endpoint.second);
   641→
   642→                used[wi] = true;
   643→                path.push_back({wi, is_last});
   644→
   645→                if (try_close(first_key, new_key, path, depth + 1)) return true;
   646→
   647→                path.pop_back();
   648→                used[wi] = false;
   649→            }
   650→            return false;
   651→        }
   652→    };
   653→
   654→    BacktrackState bt{sub_ways, coord_adj, used, coord_key, 0};
   655→
   656→    // Pass 1: Greedy (fast, O(n) per ring)
   657→    for (size_t start_idx = 0; start_idx < sub_ways.size(); start_idx++) {
   658→        if (used[start_idx]) continue;
   659→
   660→        const auto& sg = sub_ways[start_idx];
   661→        int64_t first_key = coord_key(sg.coords.front().first, sg.coords.front().second);
   662→        int64_t last_key = coord_key(sg.coords.back().first, sg.coords.back().second);
   663→
   664→        if (sg.coords.size() >= 4 && first_key == last_key) {
   665→            used[start_idx] = true;
   666→            rings.push_back(sg.coords);
   667→            continue;
   668→        }
   669→
   670→        used[start_idx] = true;
   671→        std::vector<size_t> attempt_used = {start_idx};
   672→        std::vector<std::pair<double,double>> ring = sg.coords;
   673→        int64_t current_last = last_key;
   674→        bool extended = true;
   675→        while (extended && current_last != first_key) {
   676→            extended = false;
   677→            auto it = coord_adj.find(current_last);
   678→            if (it == coord_adj.end()) break;
   679→            for (const auto& [wi, is_last] : it->second) {
   680→                if (used[wi]) continue;
   681→                const auto& sw = sub_ways[wi];
   682→                bool reversed = is_last;
   683→                auto& endpoint = reversed ? sw.coords.front() : sw.coords.back();
   684→                used[wi] = true;
   685→                attempt_used.push_back(wi);
   686→                if (!reversed) ring.insert(ring.end(), sw.coords.begin() + 1, sw.coords.end());
   687→                else for (auto rit = sw.coords.rbegin() + 1; rit != sw.coords.rend(); ++rit) ring.push_back(*rit);
   688→                current_last = coord_key(endpoint.first, endpoint.second);
   689→                extended = true;
   690→                break;
   691→            }
   692→        }
   693→        if (ring.size() >= 4 && current_last == first_key &&
   694→            !ring_has_self_intersection(ring)) {
   695→            rings.push_back(std::move(ring));
   696→        } else {
   697→            for (auto wi : attempt_used) used[wi] = false;
   698→        }
   699→    }
   700→
   701→    // Pass 2: If greedy left unused sub-ways, retry from scratch with backtracking.
   702→    // Backtracking makes correct branch choices but is slower.
   703→    {
   704→        bool any_unused = false;
   705→        for (size_t i = 0; i < used.size(); i++) {
   706→            if (!used[i]) { any_unused = true; break; }
   707→        }
   708→        if (any_unused && sub_ways.size() <= 30) {
   709→            // Only retry for small relations — large ones stall the build
   710→            std::vector<bool> bt_used(sub_ways.size(), false);
   711→            std::vector<std::vector<std::pair<double,double>>> bt_rings;
   712→
   713→            BacktrackState bt2{sub_ways, coord_adj, bt_used, coord_key, 0};
   714→            int total_bt_calls = 0;
   715→            constexpr int MAX_TOTAL_BT_CALLS = 500000; // total budget for entire relation
   716→
   717→            for (size_t si = 0; si < sub_ways.size(); si++) {
   718→                if (bt_used[si]) continue;
   719→                if (total_bt_calls >= MAX_TOTAL_BT_CALLS) break; // budget exceeded
   720→                const auto& sg = sub_ways[si];
   721→                int64_t fk = coord_key(sg.coords.front().first, sg.coords.front().second);
   722→                int64_t lk = coord_key(sg.coords.back().first, sg.coords.back().second);
   723→                if (sg.coords.size() >= 4 && fk == lk) {
   724→                    bt_used[si] = true;
   725→                    bt_rings.push_back(sg.coords);
   726→                    continue;
   727→                }
   728→                bt_used[si] = true;
   729→                bt2.calls = 0;
   730→                std::vector<std::pair<size_t, bool>> path;
   731→                if (bt2.try_close(fk, lk, path, 0)) {
   732→                    std::vector<std::pair<double,double>> r = sg.coords;
   733→                    for (const auto& [wi, rev] : path) {
   734→                        const auto& c = sub_ways[wi].coords;
   735→                        if (!rev) r.insert(r.end(), c.begin() + 1, c.end());
   736→                        else for (auto it2 = c.rbegin() + 1; it2 != c.rend(); ++it2) r.push_back(*it2);
   737→                    }
   738→                    if (r.size() >= 4 && !ring_has_self_intersection(r)) {
   739→                        bt_rings.push_back(std::move(r));
   740→                        continue;
   741→                    }
   742→                }
   743→                bt_used[si] = false;
   744→                total_bt_calls += bt2.calls;
   745→            }
   746→
   747→            // Keep whichever pass produced more rings
   748→            if (bt_rings.size() > rings.size()) {
   749→                rings = std::move(bt_rings);

{
    // Coordinate-based ring assembly: split ways at shared internal nodes,
    // then stitch sub-ways at endpoints with backtracking.
    // This matches osmium's segment-level approach.

    // Coordinate key matching osmium's Location (int32_t nanodegrees)
    auto coord_key = [](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);
    };

    // Collect coordinates from member ways.
    // First pass uses outer/empty-role ways only. If that produces 0 rings,
    // retry with ALL ways (matching osmium's check_roles=false fallback) —
    // inner ways sometimes bridge gaps between outer ways.
    std::unordered_map<int64_t, int> coord_count;
    std::vector<const std::vector<std::pair<double,double>>*> way_coords;
    for (const auto& [way_id, role] : members) {
        if (!include_all_roles && role != "outer" && !role.empty()) continue;
        auto it = way_geoms.find(way_id);
        if (it == way_geoms.end() || it->second.coords.empty()) continue;
        way_coords.push_back(&it->second.coords);
        for (const auto& [lat, lng] : it->second.coords) {
            coord_count[coord_key(lat, lng)]++;
        }
    }

    // A coordinate is a "split point" if it appears in 2+ ways' node lists.
    // Endpoints always count, internal nodes that appear in multiple ways need splitting.
    // Also, all way endpoints are natural split points.
    std::unordered_set<int64_t> split_points;
    for (auto* coords : way_coords) {
        split_points.insert(coord_key(coords->front().first, coords->front().second));
        split_points.insert(coord_key(coords->back().first, coords->back().second));
    }
    for (auto& [ck, cnt] : coord_count) {
        if (cnt > 1) split_points.insert(ck);
    }

    // Split each way at split points to create sub-ways
    struct SubWay {
        std::vector<std::pair<double,double>> coords;
    };
    std::vector<SubWay> sub_ways;
    for (auto* coords : way_coords) {
        size_t seg_start = 0;
        for (size_t i = 1; i < coords->size(); i++) {
            int64_t ck = coord_key((*coords)[i].first, (*coords)[i].second);
            if (split_points.count(ck) && i > seg_start) {
                // Create sub-way from seg_start to i (inclusive)
                SubWay sw;
                sw.coords.assign(coords->begin() + seg_start, coords->begin() + i + 1);
                if (sw.coords.size() >= 2) {
                    sub_ways.push_back(std::move(sw));
                }
                seg_start = i;
            }
        }
        // Handle remaining segment (if not already added)
        if (seg_start < coords->size() - 1) {
            SubWay sw;
            sw.coords.assign(coords->begin() + seg_start, coords->end());
            if (sw.coords.size() >= 2) {
                sub_ways.push_back(std::move(sw));
            }
        }
    }

    // Build adjacency by sub-way coordinate endpoints
    struct WayRef {
        int64_t way_id;
        const ParsedData::WayGeometry* geom; // unused for sub-ways
    };
    // We'll adapt the BacktrackState to work with sub_ways directly
    std::unordered_map<int64_t, std::vector<std::pair<size_t, bool>>> coord_adj;
    for (size_t i = 0; i < sub_ways.size(); i++) {
        auto& first = sub_ways[i].coords.front();
        auto& last = sub_ways[i].coords.back();
        coord_adj[coord_key(first.first, first.second)].push_back({i, false});
        coord_adj[coord_key(last.first, last.second)].push_back({i, true});
    }

    std::vector<bool> used(sub_ways.size(), false);
    std::vector<std::vector<std::pair<double,double>>> rings;

    // Ring closure: greedy first (fast), then full backtracking retry from
    // scratch if greedy left unused sub-ways. The second pass gets clean
    // used flags so backtracking makes optimal branch choices.

    // Backtracking with depth limit (for fallback only)
    struct BacktrackState {
        const std::vector<SubWay>& sways;
        const std::unordered_map<int64_t, std::vector<std::pair<size_t, bool>>>& adj;
        std::vector<bool>& used;
        decltype(coord_key)& key_fn;
        int calls;

        bool try_close(int64_t first_key, int64_t last_key,
                       std::vector<std::pair<size_t, bool>>& path, int depth) {
            if (++calls > 100000) return false; // limit total work, not just depth
            if (!path.empty() && first_key == last_key) return true;
            if (depth > 200) return false;

            auto it = adj.find(last_key);
            if (it == adj.end()) return false;

            for (const auto& [wi, is_last] : it->second) {
                if (used[wi]) continue;
                auto& endpoint = is_last ? sways[wi].coords.front() : sways[wi].coords.back();
                int64_t new_key = key_fn(endpoint.first, endpoint.second);

                used[wi] = true;
                path.push_back({wi, is_last});

                if (try_close(first_key, new_key, path, depth + 1)) return true;

                path.pop_back();
                used[wi] = false;
            }
            return false;
        }
    };

    BacktrackState bt{sub_ways, coord_adj, used, coord_key, 0};

    // Pass 1: Greedy (fast, O(n) per ring)
    for (size_t start_idx = 0; start_idx < sub_ways.size(); start_idx++) {
        if (used[start_idx]) continue;

        const auto& sg = sub_ways[start_idx];
        int64_t first_key = coord_key(sg.coords.front().first, sg.coords.front().second);
        int64_t last_key = coord_key(sg.coords.back().first, sg.coords.back().second);

        if (sg.coords.size() >= 4 && first_key == last_key) {
            used[start_idx] = true;
            rings.push_back(sg.coords);
            continue;
        }

        used[start_idx] = true;
        std::vector<size_t> attempt_used = {start_idx};
        std::vector<std::pair<double,double>> ring = sg.coords;
        int64_t current_last = last_key;
        bool extended = true;
        while (extended && current_last != first_key) {
            extended = false;
            auto it = coord_adj.find(current_last);
            if (it == coord_adj.end()) break;
            for (const auto& [wi, is_last] : it->second) {
                if (used[wi]) continue;
                const auto& sw = sub_ways[wi];
                bool reversed = is_last;
                auto& endpoint = reversed ? sw.coords.front() : sw.coords.back();
                used[wi] = true;
                attempt_used.push_back(wi);
                if (!reversed) ring.insert(ring.end(), sw.coords.begin() + 1, sw.coords.end());
                else for (auto rit = sw.coords.rbegin() + 1; rit != sw.coords.rend(); ++rit) ring.push_back(*rit);
                current_last = coord_key(endpoint.first, endpoint.second);
                extended = true;
                break;
            }
        }
        if (ring.size() >= 4 && current_last == first_key &&
            !ring_has_self_intersection(ring)) {
            rings.push_back(std::move(ring));
        } else {
            for (auto wi : attempt_used) used[wi] = false;
        }
    }

    // Pass 2: If greedy left unused sub-ways, retry from scratch with backtracking.
    // Backtracking makes correct branch choices but is slower.
    {
        bool any_unused = false;
        for (size_t i = 0; i < used.size(); i++) {
            if (!used[i]) { any_unused = true; break; }
        }
        if (any_unused && sub_ways.size() <= 30) {
            // Only retry for small relations — large ones stall the build
            std::vector<bool> bt_used(sub_ways.size(), false);
            std::vector<std::vector<std::pair<double,double>>> bt_rings;

            BacktrackState bt2{sub_ways, coord_adj, bt_used, coord_key, 0};
            int total_bt_calls = 0;
            constexpr int MAX_TOTAL_BT_CALLS = 500000; // total budget for entire relation

            for (size_t si = 0; si < sub_ways.size(); si++) {
                if (bt_used[si]) continue;
                if (total_bt_calls >= MAX_TOTAL_BT_CALLS) break; // budget exceeded
                const auto& sg = sub_ways[si];
                int64_t fk = coord_key(sg.coords.front().first, sg.coords.front().second);
                int64_t lk = coord_key(sg.coords.back().first, sg.coords.back().second);
                if (sg.coords.size() >= 4 && fk == lk) {
                    bt_used[si] = true;
                    bt_rings.push_back(sg.coords);
                    continue;
                }
                bt_used[si] = true;
                bt2.calls = 0;
                std::vector<std::pair<size_t, bool>> path;
                if (bt2.try_close(fk, lk, path, 0)) {
                    std::vector<std::pair<double,double>> r = sg.coords;
                    for (const auto& [wi, rev] : path) {
                        const auto& c = sub_ways[wi].coords;
                        if (!rev) r.insert(r.end(), c.begin() + 1, c.end());
                        else for (auto it2 = c.rbegin() + 1; it2 != c.rend(); ++it2) r.push_back(*it2);
                    }
                    if (r.size() >= 4 && !ring_has_self_intersection(r)) {
                        bt_rings.push_back(std::move(r));
                        continue;
                    }
                }
                bt_used[si] = false;
                total_bt_calls += bt2.calls;
            }

            // Keep whichever pass produced more rings
            if (bt_rings.size() > rings.size()) {
                rings = std::move(bt_rings);


This is a lot of code to replace. Let me write the new osmium-style assembler in a clean function.


Edit

{
    // Coordinate-based ring assembly: split ways at shared internal nodes,
    // then stitch sub-ways at endpoints with backtracking.
    // This matches osmium's segment-level approach.

    // Coordinate key matching osmium's Location (int32_t nanodegrees)
    auto coord_key = [](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);
    };

    // Collect coordinates from member ways.
    // First pass uses outer/empty-role ways only. If that produces 0 rings,
    // retry with ALL ways (matching osmium's check_roles=false fallback) —
    // inner ways sometimes bridge gaps between outer ways.
    std::unordered_map<int64_t, int> coord_count;
    std::vector<const std::vector<std::pair<double,double>>*> way_coords;
    for (const auto& [way_id, role] : members) {
        if (!include_all_roles && role != "outer" && !role.empty()) continue;
        auto it = way_geoms.find(way_id);
        if (it == way_geoms.end() || it->second.coords.empty()) continue;
        way_coords.push_back(&it->second.coords);
        for (const auto& [lat, lng] : it->second.coords) {
            coord_count[coord_key(lat, lng)]++;
        }
    }

    // A coordinate is a "split point" if it appears in 2+ ways' node lists.
    // Endpoints always count, internal nodes that appear in multiple ways need splitting.
    // Also, all way endpoints are natural split points.
    std::unordered_set<int64_t> split_points;
    for (auto* coords : way_coords) {
        split_points.insert(coord_key(coords->front().first, coords->front().second));
        split_points.insert(coord_key(coords->back().first, coords->back().second));
    }
    for (auto& [ck, cnt] : coord_count) {
        if (cnt > 1) split_points.insert(ck);
    }

    // Split each way at split points to create sub-ways
    struct SubWay {
        std::vector<std::pair<double,double>> coords;
    };
    std::vector<SubWay> sub_ways;
    for (auto* coords : way_coords) {
        size_t seg_start = 0;
        for (size_t i = 1; i < coords->size(); i++) {
            int64_t ck = coord_key((*coords)[i].first, (*coords)[i].second);
            if (split_points.count(ck) && i > seg_start) {
                // Create sub-way from seg_start to i (inclusive)
                SubWay sw;
                sw.coords.assign(coords->begin() + seg_start, coords->begin() + i + 1);
                if (sw.coords.size() >= 2) {
                    sub_ways.push_back(std::move(sw));
                }
                seg_start = i;
            }
        }
        // Handle remaining segment (if not already added)
        if (seg_start < coords->size() - 1) {
            SubWay sw;
            sw.coords.assign(coords->begin() + seg_start, coords->end());
            if (sw.coords.size() >= 2) {
                sub_ways.push_back(std::move(sw));
            }
        }
    }

    // Build adjacency by sub-way coordinate endpoints
    struct WayRef {
        int64_t way_id;
        const ParsedData::WayGeometry* geom; // unused for sub-ways
    };
    // We'll adapt the BacktrackState to work with sub_ways directly
    std::unordered_map<int64_t, std::vector<std::pair<size_t, bool>>> coord_adj;
    for (size_t i = 0; i < sub_ways.size(); i++) {
        auto& first = sub_ways[i].coords.front();
        auto& last = sub_ways[i].coords.back();
        coord_adj[coord_key(first.first, first.second)].push_back({i, false});
        coord_adj[coord_key(last.first, last.second)].push_back({i, true});
    }

    std::vector<bool> used(sub_ways.size(), false);
    std::vector<std::vector<std::pair<double,double>>> rings;

    // Ring closure: greedy first (fast), then full backtracking retry from
    // scratch if greedy left unused sub-ways. The second pass gets clean
    // used flags so backtracking makes optimal branch choices.

    // Backtracking with depth limit (for fallback only)
    struct BacktrackState {
        const std::vector<SubWay>& sways;
        const std::unordered_map<int64_t, std::vector<std::pair<size_t, bool>>>& adj;
        std::vector<bool>& used;
        decltype(coord_key)& key_fn;
        int calls;

        bool try_close(int64_t first_key, int64_t last_key,
                       std::vector<std::pair<size_t, bool>>& path, int depth) {
            if (++calls > 100000) return false; // limit total work, not just depth
            if (!path.empty() && first_key == last_key) return true;
            if (depth > 200) return false;

            auto it = adj.find(last_key);
            if (it == adj.end()) return false;

            for (const auto& [wi, is_last] : it->second) {
                if (used[wi]) continue;
                auto& endpoint = is_last ? sways[wi].coords.front() : sways[wi].coords.back();
                int64_t new_key = key_fn(endpoint.first, endpoint.second);

                used[wi] = true;
                path.push_back({wi, is_last});

                if (try_close(first_key, new_key, path, depth + 1)) return true;

                path.pop_back();
                used[wi] = false;
            }
            return false;
        }
    };

    BacktrackState bt{sub_ways, coord_adj, used, coord_key, 0};

    // Pass 1: Greedy (fast, O(n) per ring)
    for (size_t start_idx = 0; start_idx < sub_ways.size(); start_idx++) {
        if (used[start_idx]) continue;

        const auto& sg = sub_ways[start_idx];
        int64_t first_key = coord_key(sg.coords.front().first, sg.coords.front().second);
        int64_t last_key = coord_key(sg.coords.back().first, sg.coords.back().second);

        if (sg.coords.size() >= 4 && first_key == last_key) {
            used[start_idx] = true;
            rings.push_back(sg.coords);
            continue;
        }

        used[start_idx] = true;
        std::vector<size_t> attempt_used = {start_idx};
        std::vector<std::pair<double,double>> ring = sg.coords;
        int64_t current_last = last_key;
        bool extended = true;
        while (extended && current_last != first_key) {
            extended = false;
            auto it = coord_adj.find(current_last);
            if (it == coord_adj.end()) break;
            for (const auto& [wi, is_last] : it->second) {
                if (used[wi]) continue;
                const auto& sw = sub_ways[wi];
                bool reversed = is_last;
                auto& endpoint = reversed ? sw.coords.front() : sw.coords.back();
                used[wi] = true;
                attempt_used.push_back(wi);
                if (!reversed) ring.insert(ring.end(), sw.coords.begin() + 1, sw.coords.end());
                else for (auto rit = sw.coords.rbegin() + 1; rit != sw.coords.rend(); ++rit) ring.push_back(*rit);
                current_last = coord_key(endpoint.first, endpoint.second);
                extended = true;
                break;
            }
        }
        if (ring.size() >= 4 && current_last == first_key &&
            !ring_has_self_intersection(ring)) {
            rings.push_back(std::move(ring));
        } else {
            for (auto wi : attempt_used) used[wi] = false;
        }
    }

    // Pass 2: If greedy left unused sub-ways, retry from scratch with backtracking.
    // Backtracking makes correct branch choices but is slower.
    {
        bool any_unused = false;
        for (size_t i = 0; i < used.size(); i++) {
            if (!used[i]) { any_unused = true; break; }
        }
        if (any_unused && sub_ways.size() <= 30) {
            // Only retry for small relations — large ones stall the build
            std::vector<bool> bt_used(sub_ways.size(), false);
            std::vector<std::vector<std::pair<double,double>>> bt_rings;

            BacktrackState bt2{sub_ways, coord_adj, bt_used, coord_key, 0};
            int total_bt_calls = 0;
            constexpr int MAX_TOTAL_BT_CALLS = 500000; // total budget for entire relation

            for (size_t si = 0; si < sub_ways.size(); si++) {
                if (bt_used[si]) continue;
                if (total_bt_calls >= MAX_TOTAL_BT_CALLS) break; // budget exceeded
                const auto& sg = sub_ways[si];
                int64_t fk = coord_key(sg.coords.front().first, sg.coords.front().second);
                int64_t lk = coord_key(sg.coords.back().first, sg.coords.back().second);
                if (sg.coords.size() >= 4 && fk == lk) {
                    bt_used[si] = true;
                    bt_rings.push_back(sg.coords);
                    continue;
                }
                bt_used[si] = true;
                bt2.calls = 0;
                std::vector<std::pair<size_t, bool>> path;
                if (bt2.try_close(fk, lk, path, 0)) {
                    std::vector<std::pair<double,double>> r = sg.coords;
                    for (const auto& [wi, rev] : path) {
                        const auto& c = sub_ways[wi].coords;
                        if (!rev) r.insert(r.end(), c.begin() + 1, c.end());
                        else for (auto it2 = c.rbegin() + 1; it2 != c.rend(); ++it2) r.push_back(*it2);
                    }
                    if (r.size() >= 4 && !ring_has_self_intersection(r)) {
                        bt_rings.push_back(std::move(r));
                        continue;
                    }
                }
                bt_used[si] = false;
                total_bt_calls += bt2.calls;
            }

            // Keep whichever pass produced more rings
            if (bt_rings.size() > rings.size()) {
                rings = std::move(bt_rings);
            }
        }
    }

    return rings;
}

{
    // Osmium-style segment-graph ring assembly.
    // Coordinate key matching osmium's Location (int32_t nanodegrees)
    auto coord_key = [](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);
    };

    // Step 1: Decompose all member ways into segments (consecutive coordinate pairs).
    // Each segment is directed: start -> stop. We store both coordinates.
    struct Segment {
        int64_t start_key, stop_key;
        double start_lat, start_lng, stop_lat, stop_lng;
        bool done = false;

        void reverse() {
            std::swap(start_key, stop_key);
            std::swap(start_lat, stop_lat);
            std::swap(start_lng, stop_lng);
        }
    };

    std::vector<Segment> segments;
    for (const auto& [way_id, role] : members) {
        if (!include_all_roles && role != "outer" && !role.empty()) continue;
        auto it = way_geoms.find(way_id);
        if (it == way_geoms.end() || it->second.coords.size() < 2) continue;
        const auto& coords = it->second.coords;
        for (size_t i = 0; i + 1 < coords.size(); i++) {
            int64_t k1 = coord_key(coords[i].first, coords[i].second);
            int64_t k2 = coord_key(coords[i+1].first, coords[i+1].second);
            if (k1 == k2) continue; // skip zero-length
            // Normalize: smaller key first (like osmium's segment ordering)
            if (k1 <= k2) {
                segments.push_back({k1, k2, coords[i].first, coords[i].second,
                                    coords[i+1].first, coords[i+1].second, false});
            } else {
                segments.push_back({k2, k1, coords[i+1].first, coords[i+1].second,
                                    coords[i].first, coords[i].second, false});
            }
        }
    }

    if (segments.empty()) return {};

    // Step 2: Sort segments and remove duplicate pairs (shared boundaries cancel).
    std::sort(segments.begin(), segments.end(), [](const Segment& a, const Segment& b) {
        if (a.start_key != b.start_key) return a.start_key < b.start_key;
        return a.stop_key < b.stop_key;
    });

    // Remove duplicate pairs (like osmium's erase_duplicate_segments)
    {
        size_t write = 0;
        size_t i = 0;
        while (i < segments.size()) {
            if (i + 1 < segments.size() &&
                segments[i].start_key == segments[i+1].start_key &&
                segments[i].stop_key == segments[i+1].stop_key) {
                i += 2; // skip pair
            } else {
                segments[write++] = segments[i++];
            }
        }
        segments.resize(write);
    }

    if (segments.empty()) return {};

    // Step 3: Build sorted location index.
    // Each segment has two endpoints. Create entries for both.
    struct SLoc {
        uint32_t seg_idx;
        bool is_stop; // false = start endpoint, true = stop endpoint
        int64_t key() const { return 0; } // filled by sort
    };

    // Build location list: for each segment, two entries (start, stop)
    struct LocEntry {
        int64_t key;
        uint32_t seg_idx;
        bool is_stop; // which end of the segment this entry represents
    };

    std::vector<LocEntry> locations;
    locations.reserve(segments.size() * 2);
    for (uint32_t i = 0; i < segments.size(); i++) {
        locations.push_back({segments[i].start_key, i, false});
        locations.push_back({segments[i].stop_key, i, true});
    }
    std::sort(locations.begin(), locations.end(), [](const LocEntry& a, const LocEntry& b) {
        return a.key < b.key;
    });

    // Step 4: Find split locations (keys where 3+ segment endpoints meet).
    std::unordered_set<int64_t> split_locs;
    for (size_t i = 0; i + 2 < locations.size(); i++) {
        if (locations[i].key == locations[i+1].key && locations[i+1].key == locations[i+2].key) {
            split_locs.insert(locations[i].key);
        }
    }

    // Check for open rings (unpaired endpoints = locations appearing odd times)
    bool has_open_rings = false;
    for (size_t i = 0; i < locations.size(); ) {
        size_t j = i;
        while (j < locations.size() && locations[j].key == locations[i].key) j++;
        if ((j - i) % 2 != 0) has_open_rings = true;
        i = j;
    }

    // Helper: find next unused segment at a given location
    auto get_next_segment = [&](int64_t loc_key) -> int32_t {
        auto it = std::lower_bound(locations.begin(), locations.end(), loc_key,
            [](const LocEntry& e, int64_t k) { return e.key < k; });
        while (it != locations.end() && it->key == loc_key) {
            if (!segments[it->seg_idx].done) return static_cast<int32_t>(it - locations.begin());
            ++it;
        }
        return -1;
    };

    std::vector<std::vector<std::pair<double,double>>> rings;

    // Step 5: Trace rings.
    auto trace_ring = [&](int32_t start_loc_idx, bool stop_at_splits) -> std::vector<std::pair<double,double>> {
        auto& start_entry = locations[start_loc_idx];
        auto& seg = segments[start_entry.seg_idx];
        if (seg.done) return {};

        seg.done = true;
        // Orient segment so we enter from the location entry's endpoint
        if (start_entry.is_stop) seg.reverse();

        std::vector<std::pair<double,double>> ring;
        ring.push_back({seg.start_lat, seg.start_lng});

        int64_t first_key = seg.start_key;
        int64_t last_key = seg.stop_key;
        ring.push_back({seg.stop_lat, seg.stop_lng});

        while (last_key != first_key) {
            if (stop_at_splits && split_locs.count(last_key)) break;

            int32_t next_idx = get_next_segment(last_key);
            if (next_idx < 0) break;

            auto& next_entry = locations[next_idx];
            auto& next_seg = segments[next_entry.seg_idx];
            next_seg.done = true;

            // Orient: we enter from last_key
            if (next_entry.is_stop) next_seg.reverse();
            assert(next_seg.start_key == last_key);

            last_key = next_seg.stop_key;
            ring.push_back({next_seg.stop_lat, next_seg.stop_lng});
        }

        return ring;
    };

    if (split_locs.empty() && !has_open_rings) {
        // Simple case: no splits, no open rings. Trace closed rings directly.
        for (size_t i = 0; i < locations.size(); i++) {
            if (segments[locations[i].seg_idx].done) continue;
            auto ring = trace_ring(static_cast<int32_t>(i), false);
            if (ring.size() >= 4) rings.push_back(std::move(ring));
        }
    } else {
        // Complex case: trace partial rings from split locations first,
        // then remaining segments, then join open rings.

        // Trace from split locations
        std::vector<std::vector<std::pair<double,double>>> partial_rings;
        for (int64_t split_key : split_locs) {
            int32_t idx = get_next_segment(split_key);
            while (idx >= 0) {
                auto ring = trace_ring(idx, true);
                if (!ring.empty()) {
                    int64_t rk_first = coord_key(ring.front().first, ring.front().second);
                    int64_t rk_last = coord_key(ring.back().first, ring.back().second);
                    if (ring.size() >= 4 && rk_first == rk_last) {
                        rings.push_back(std::move(ring));
                    } else {
                        partial_rings.push_back(std::move(ring));
                    }
                }
                idx = get_next_segment(split_key);
            }
        }

        // Trace remaining (non-split) segments
        for (size_t i = 0; i < locations.size(); i++) {
            if (segments[locations[i].seg_idx].done) continue;
            auto ring = trace_ring(static_cast<int32_t>(i), true);
            if (!ring.empty()) {
                int64_t rk_first = coord_key(ring.front().first, ring.front().second);
                int64_t rk_last = coord_key(ring.back().first, ring.back().second);
                if (ring.size() >= 4 && rk_first == rk_last) {
                    rings.push_back(std::move(ring));
                } else {
                    partial_rings.push_back(std::move(ring));
                }
            }
        }

        // Join open partial rings by matching endpoints
        bool joined = true;
        while (joined) {
            joined = false;
            // Build endpoint index for partial rings
            std::unordered_map<int64_t, std::vector<size_t>> endpoint_to_ring;
            for (size_t i = 0; i < partial_rings.size(); i++) {
                if (partial_rings[i].empty()) continue;
                int64_t fk = coord_key(partial_rings[i].front().first, partial_rings[i].front().second);
                int64_t lk = coord_key(partial_rings[i].back().first, partial_rings[i].back().second);
                endpoint_to_ring[fk].push_back(i);
                endpoint_to_ring[lk].push_back(i);
            }

            // Try to join pairs that share an endpoint
            for (auto& [key, ring_indices] : endpoint_to_ring) {
                if (ring_indices.size() < 2) continue;
                // Find two non-empty rings
                size_t ri = SIZE_MAX, rj = SIZE_MAX;
                for (auto idx : ring_indices) {
                    if (partial_rings[idx].empty()) continue;
                    if (ri == SIZE_MAX) ri = idx;
                    else if (rj == SIZE_MAX && idx != ri) { rj = idx; break; }
                }
                if (ri == SIZE_MAX || rj == SIZE_MAX) continue;

                auto& ra = partial_rings[ri];
                auto& rb = partial_rings[rj];
                int64_t ra_first = coord_key(ra.front().first, ra.front().second);
                int64_t ra_last = coord_key(ra.back().first, ra.back().second);
                int64_t rb_first = coord_key(rb.front().first, rb.front().second);
                int64_t rb_last = coord_key(rb.back().first, rb.back().second);

                // Join: ra_last == rb_first → append rb to ra
                if (ra_last == rb_first) {
                    ra.insert(ra.end(), rb.begin() + 1, rb.end());
                    rb.clear();
                } else if (ra_last == rb_last) {
                    // Reverse rb and append
                    for (auto rit = rb.rbegin() + 1; rit != rb.rend(); ++rit)
                        ra.push_back(*rit);
                    rb.clear();
                } else if (ra_first == rb_last) {
                    rb.insert(rb.end(), ra.begin() + 1, ra.end());
                    ra = std::move(rb);
                    rb.clear();
                } else if (ra_first == rb_first) {
                    std::reverse(rb.begin(), rb.end());
                    rb.insert(rb.end(), ra.begin() + 1, ra.end());
                    ra = std::move(rb);
                    rb.clear();
                }

                // Check if now closed
                if (!ra.empty()) {
                    int64_t fk = coord_key(ra.front().first, ra.front().second);
                    int64_t lk = coord_key(ra.back().first, ra.back().second);
                    if (ra.size() >= 4 && fk == lk) {
                        rings.push_back(std::move(ra));
                        ra.clear();
                    }
                }
                joined = true;
                break; // restart after each join (indices changed)
            }
        }
    }

    return rings;
}