Now swap the order: try backtracking first, greedy second.


   643→        }
   644→    };
   645→
   646→    BacktrackState bt{sub_ways, coord_adj, used, coord_key, 0};
   647→
   648→    for (size_t start_idx = 0; start_idx < sub_ways.size(); start_idx++) {
   649→        if (used[start_idx]) continue;
   650→
   651→        const auto& sg = sub_ways[start_idx];
   652→        int64_t first_key = coord_key(sg.coords.front().first, sg.coords.front().second);
   653→        int64_t last_key = coord_key(sg.coords.back().first, sg.coords.back().second);
   654→
   655→        // Single sub-way already closed
   656→        if (sg.coords.size() >= 4 && first_key == last_key) {
   657→            used[start_idx] = true;
   658→            rings.push_back(sg.coords);
   659→            continue;
   660→        }
   661→
   662→        // Greedy extension: follow the chain, tracking what we use
   663→        used[start_idx] = true;
   664→        std::vector<size_t> attempt_used = {start_idx};
   665→        std::vector<std::pair<double,double>> ring = sg.coords;
   666→        int64_t current_last = last_key;
   667→        bool extended = true;
   668→        while (extended && current_last != first_key) {
   669→            extended = false;
   670→            auto it = coord_adj.find(current_last);
   671→            if (it == coord_adj.end()) break;
   672→
   673→            for (const auto& [wi, is_last] : it->second) {
   674→                if (used[wi]) continue;
   675→                const auto& sw = sub_ways[wi];
   676→                bool reversed = is_last;
   677→                auto& endpoint = reversed ? sw.coords.front() : sw.coords.back();
   678→                int64_t new_key = coord_key(endpoint.first, endpoint.second);
   679→
   680→                used[wi] = true;
   681→                attempt_used.push_back(wi);
   682→                if (!reversed) {
   683→                    ring.insert(ring.end(), sw.coords.begin() + 1, sw.coords.end());
   684→                } else {
   685→                    for (auto rit = sw.coords.rbegin() + 1; rit != sw.coords.rend(); ++rit) {
   686→                        ring.push_back(*rit);
   687→                    }
   688→                }
   689→                current_last = new_key;
   690→                extended = true;
   691→                break;
   692→            }
   693→        }
   694→
   695→        if (ring.size() >= 4 && current_last == first_key &&
   696→            !ring_has_self_intersection(ring)) {
   697→            rings.push_back(std::move(ring));
   698→            continue;
   699→        }
   700→
   701→        // Greedy failed — reset and try backtracking with limited work budget
   702→        for (auto wi : attempt_used) used[wi] = false;
   703→
   704→        used[start_idx] = true;
   705→        bt.calls = 0;
   706→        std::vector<std::pair<size_t, bool>> path;
   707→        if (bt.try_close(first_key, last_key, path, 0)) {
   708→            std::vector<std::pair<double,double>> bt_ring = sg.coords;
   709→            for (const auto& [wi, reversed] : path) {
   710→                const auto& coords = sub_ways[wi].coords;
   711→                if (!reversed) {
   712→                    bt_ring.insert(bt_ring.end(), coords.begin() + 1, coords.end());
   713→                } else {
   714→                    for (auto rit = coords.rbegin() + 1; rit != coords.rend(); ++rit) {
   715→                        bt_ring.push_back(*rit);
   716→                    }
   717→                }
   718→            }
   719→            if (bt_ring.size() >= 4 && !ring_has_self_intersection(bt_ring)) {
   720→                rings.push_back(std::move(bt_ring));
   721→                continue;
   722→            }
   723→        }
   724→
   725→        // Both failed — reset
   726→        used[start_idx] = false;
   727→    }
   728→
   729→    // Second pass: try backtracking on remaining unused sub-ways.
   730→    // The greedy pass may have consumed sub-ways needed by other rings.
   731→    // Reset all unused flags and try again with backtracking only.
   732→    { // Second pass: backtracking retry (100K call budget prevents exponential blowup)
   733→        bool any_unused = false;
   734→        for (size_t i = 0; i < used.size(); i++) {
   735→            if (!used[i]) { any_unused = true; break; }
   736→        }
   737→        if (any_unused) {
   738→            // Reset ALL used flags and try pure backtracking
   739→            std::fill(used.begin(), used.end(), false);
   740→            // Re-mark already-found rings' sub-ways as used
   741→            // (we can't easily do this, so just re-run from scratch)
   742→            std::vector<std::vector<std::pair<double,double>>> saved_rings = std::move(rings);

        }
    };

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

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

        // Single sub-way already closed
        if (sg.coords.size() >= 4 && first_key == last_key) {
            used[start_idx] = true;
            rings.push_back(sg.coords);
            continue;
        }

        // Greedy extension: follow the chain, tracking what we use
        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();
                int64_t new_key = coord_key(endpoint.first, endpoint.second);

                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 = new_key;
                extended = true;
                break;
            }
        }

        if (ring.size() >= 4 && current_last == first_key &&
            !ring_has_self_intersection(ring)) {
            rings.push_back(std::move(ring));
            continue;
        }

        // Greedy failed — reset and try backtracking with limited work budget
        for (auto wi : attempt_used) used[wi] = false;

        used[start_idx] = true;
        bt.calls = 0;
        std::vector<std::pair<size_t, bool>> path;
        if (bt.try_close(first_key, last_key, path, 0)) {
            std::vector<std::pair<double,double>> bt_ring = sg.coords;
            for (const auto& [wi, reversed] : path) {
                const auto& coords = sub_ways[wi].coords;
                if (!reversed) {
                    bt_ring.insert(bt_ring.end(), coords.begin() + 1, coords.end());
                } else {
                    for (auto rit = coords.rbegin() + 1; rit != coords.rend(); ++rit) {
                        bt_ring.push_back(*rit);
                    }
                }
            }
            if (bt_ring.size() >= 4 && !ring_has_self_intersection(bt_ring)) {
                rings.push_back(std::move(bt_ring));
                continue;
            }
        }

        // Both failed — reset
        used[start_idx] = false;
    }

    // Second pass: try backtracking on remaining unused sub-ways.
    // The greedy pass may have consumed sub-ways needed by other rings.
    // Reset all unused flags and try again with backtracking only.
    { // Second pass: backtracking retry (100K call budget prevents exponential blowup)
        bool any_unused = false;
        for (size_t i = 0; i < used.size(); i++) {
            if (!used[i]) { any_unused = true; break; }
        }
        if (any_unused) {
            // Reset ALL used flags and try pure backtracking
            std::fill(used.begin(), used.end(), false);
            // Re-mark already-found rings' sub-ways as used
            // (we can't easily do this, so just re-run from scratch)
            std::vector<std::vector<std::pair<double,double>>> saved_rings = std::move(rings);


Let me simplify this. Instead of the complex dual-pass, just try backtracking first, fall back to greedy if budget exceeded.


Edit

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

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

        // Single sub-way already closed
        if (sg.coords.size() >= 4 && first_key == last_key) {
            used[start_idx] = true;
            rings.push_back(sg.coords);
            continue;
        }

        // Greedy extension: follow the chain, tracking what we use
        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();
                int64_t new_key = coord_key(endpoint.first, endpoint.second);

                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 = new_key;
                extended = true;
                break;
            }
        }

        if (ring.size() >= 4 && current_last == first_key &&
            !ring_has_self_intersection(ring)) {
            rings.push_back(std::move(ring));
            continue;
        }

        // Greedy failed — reset and try backtracking with limited work budget
        for (auto wi : attempt_used) used[wi] = false;

        used[start_idx] = true;
        bt.calls = 0;
        std::vector<std::pair<size_t, bool>> path;
        if (bt.try_close(first_key, last_key, path, 0)) {
            std::vector<std::pair<double,double>> bt_ring = sg.coords;
            for (const auto& [wi, reversed] : path) {
                const auto& coords = sub_ways[wi].coords;
                if (!reversed) {
                    bt_ring.insert(bt_ring.end(), coords.begin() + 1, coords.end());
                } else {
                    for (auto rit = coords.rbegin() + 1; rit != coords.rend(); ++rit) {
                        bt_ring.push_back(*rit);
                    }
                }
            }
            if (bt_ring.size() >= 4 && !ring_has_self_intersection(bt_ring)) {
                rings.push_back(std::move(bt_ring));
                continue;
            }
        }

        // Both failed — reset
        used[start_idx] = false;
    }

    // Second pass: try backtracking on remaining unused sub-ways.
    // The greedy pass may have consumed sub-ways needed by other rings.
    // Reset all unused flags and try again with backtracking only.
    { // Second pass: backtracking retry (100K call budget prevents exponential blowup)
        bool any_unused = false;
        for (size_t i = 0; i < used.size(); i++) {
            if (!used[i]) { any_unused = true; break; }
        }
        if (any_unused) {
            // Reset ALL used flags and try pure backtracking
            std::fill(used.begin(), used.end(), false);
            // Re-mark already-found rings' sub-ways as used
            // (we can't easily do this, so just re-run from scratch)
            std::vector<std::vector<std::pair<double,double>>> saved_rings = std::move(rings);
            rings.clear();

            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 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) {
                    used[start_idx] = true;
                    rings.push_back(sg.coords);
                    continue;
                }
                used[start_idx] = true;
                bt.calls = 0;
                std::vector<std::pair<size_t, bool>> path;
                if (bt.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)) {
                        rings.push_back(std::move(r));
                        continue;
                    }
                }
                used[start_idx] = false;
            }

            // Use whichever pass produced more rings
            if (rings.size() < saved_rings.size()) {
                rings = std::move(saved_rings);
            }
        }
    }

    // Backtracking first (correct branch choices), greedy fallback (for budget-exceeded cases)
    BacktrackState bt{sub_ways, coord_adj, used, coord_key, 0};
    bool budget_exceeded = false;

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

        // Single sub-way already closed
        if (sg.coords.size() >= 4 && first_key == last_key) {
            used[start_idx] = true;
            rings.push_back(sg.coords);
            continue;
        }

        // Try backtracking first (correct at branch points)
        used[start_idx] = true;
        bt.calls = 0;
        std::vector<std::pair<size_t, bool>> path;
        if (bt.try_close(first_key, last_key, path, 0)) {
            std::vector<std::pair<double,double>> ring = sg.coords;
            for (const auto& [wi, reversed] : path) {
                const auto& coords = sub_ways[wi].coords;
                if (!reversed) {
                    ring.insert(ring.end(), coords.begin() + 1, coords.end());
                } else {
                    for (auto rit = coords.rbegin() + 1; rit != coords.rend(); ++rit) {
                        ring.push_back(*rit);
                    }
                }
            }
            if (ring.size() >= 4 && !ring_has_self_intersection(ring)) {
                rings.push_back(std::move(ring));
                continue;
            }
        }

        // Backtracking failed or budget exceeded — try greedy
        used[start_idx] = false;
        if (bt.calls >= 100000) budget_exceeded = true;

        // Greedy extension
        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;
        }
    }