Now fix the cross-border extras. The issue is our stitcher creates rings from partial data when some member ways are outside the extract. Osmium only creates areas when it has enough data for valid rings. Let me check: are the 12 extras from the ring stitcher or from closed ways?


Bash

ssh node3 "/tmp/check_admin /tmp/de-parallel-cwa 2>/dev/null" 2>&1 | head -1
# Check if the extras exist in the non-closed-way build
ssh node3 "/tmp/check_admin /tmp/de-parallel-split 2>/dev/null" 2>&1 | head -1
# Check Austria etc specifically
ssh node3 "cat > /tmp/check_extra.cpp << 'EOFCPP'
#include <cstdint>
#include <cstring>
#include <fstream>
#include <iostream>
#include <vector>
#include <set>
struct AdminPolygon { uint32_t vertex_offset; uint16_t vertex_count; uint32_t name_id; uint8_t admin_level; float area; uint16_t country_code; };
static std::vector<char> read_file(const std::string& path) {
    std::ifstream f(path, std::ios::binary | std::ios::ate); if (!f) return {};
    auto sz = f.tellg(); f.seekg(0);
    std::vector<char> d(sz); f.read(d.data(), sz); return d;
}
int main(int argc, char* argv[]) {
    std::set<std::string> targets = {\"Bezirk Reutte\", \"Tirol\", \"Zurzach\", \"Glattfelden\", \"Lobith\",
        \"Neuhaus\", \"Krefeld-Ost\", \"Krefeld-Uerdingen\"};
    for (int a = 1; a < argc; a++) {
        std::string dir = argv[a];
        auto strings = read_file(dir + \"/strings.bin\");
        auto data = read_file(dir + \"/admin_polygons.bin\");
        auto* polys = (AdminPolygon*)data.data();
        size_t count = data.size() / sizeof(AdminPolygon);
        printf(\"%s:\\n\", dir.c_str());
        for (size_t i = 0; i < count; i++) {
            const char* name = (polys[i].name_id < strings.size()) ? strings.data() + polys[i].name_id : \"?\";
            if (targets.count(name))
                printf(\"  '%s' level=%d verts=%d area=%.4f\\n\", name, polys[i].admin_level, polys[i].vertex_count, polys[i].area);
        }
    }
    return 0;
}
EOFCPP
g++ -O2 -std=c++17 -o /tmp/check_extra /tmp/check_extra.cpp && /tmp/check_extra /tmp/de-parallel-cwa /tmp/de-parallel-split /tmp/de-sequential-final" 2>&1

Find where the extra polygons come from


/tmp/de-parallel-cwa (43280 polygons):
/tmp/de-parallel-split (43193 polygons):
/tmp/de-parallel-cwa:
  'Neuhaus' level=10 verts=9 area=0.0001
  'Neuhaus' level=10 verts=416 area=0.0023
  'Neuhaus' level=10 verts=402 area=0.0004
  'Neuhaus' level=10 verts=17 area=0.0000
  'Krefeld-Uerdingen' level=9 verts=500 area=0.0028
  'Tirol' level=4 verts=492 area=0.0008
  'Glattfelden' level=8 verts=53 area=0.0000
  'Bezirk Reutte' level=6 verts=492 area=0.0008
  'Krefeld-Ost' level=9 verts=499 area=0.0036
  'Krefeld-Ost' level=9 verts=7 area=0.0000
  'Lobith' level=10 verts=108 area=0.0000
  'Zurzach' level=8 verts=21 area=0.0000
  'Neuhaus' level=10 verts=12 area=0.0000
  'Neuhaus' level=8 verts=432 area=0.0013
/tmp/de-parallel-split:
  'Neuhaus' level=10 verts=9 area=0.0001
  'Lobith' level=10 verts=108 area=0.0000
  'Neuhaus' level=10 verts=12 area=0.0000
  'Glattfelden' level=8 verts=53 area=0.0000
  'Neuhaus' level=10 verts=402 area=0.0004
  'Neuhaus' level=10 verts=17 area=0.0000
  'Krefeld-Uerdingen' level=9 verts=500 area=0.0028
  'Neuhaus' level=8 verts=432 area=0.0013
  'Bezirk Reutte' level=6 verts=492 area=0.0008
  'Tirol' level=4 verts=492 area=0.0008
  'Neuhaus' level=10 verts=416 area=0.0023
  'Zurzach' level=8 verts=21 area=0.0000
  'Krefeld-Ost' level=9 verts=499 area=0.0036
  'Krefeld-Ost' level=9 verts=7 area=0.0000
/tmp/de-sequential-final:
  'Neuhaus' level=10 verts=9 area=0.0001
  'Neuhaus' level=10 verts=416 area=0.0023
  'Krefeld-Ost' level=9 verts=499 area=0.0036
  'Krefeld-Uerdingen' level=9 verts=7 area=0.0000
  'Krefeld-Uerdingen' level=9 verts=500 area=0.0028
  'Neuhaus' level=10 verts=402 area=0.0004
  'Neuhaus' level=10 verts=17 area=0.0000
  'Neuhaus' level=8 verts=432 area=0.0013


The extras come from our stitcher creating partial rings from incomplete cross-border relations. Osmium correctly doesn't create these since the data is incomplete. The fix: skip ring assembly for relations with missing member ways.


  2415→                            const char* cc = cwa.country_code.empty() ? nullptr : cwa.country_code.c_str();
  2416→                            add_admin_polygon(data, cwa.vertices, cwa.name.c_str(),
  2417→                                              cwa.admin_level, cc, &admin_pool);
  2418→                            closed_way_admin_count++;
  2419→                        }
  2420→                        local.closed_way_admins.clear();
  2421→                    }
  2422→                    if (closed_way_admin_count > 0) {
  2423→                        std::cerr << "  Added " << closed_way_admin_count
  2424→                                  << " admin polygons from closed ways." << std::endl;
  2425→                    }
  2426→                }
  2427→
  2428→                // --- Parallel admin boundary assembly ---
  2429→                if (parallel_admin) {
  2430→                    std::cerr << "  Assembling admin polygons in parallel ("
  2431→                              << data.collected_relations.size() << " relations, "
  2432→                              << data.way_geometries.size() << " way geometries)..." << std::endl;
  2433→
  2434→                    // Thread-local admin results (to avoid locking add_admin_polygon)
  2435→                    struct AdminResult {
  2436→                        std::vector<std::pair<double,double>> vertices;
  2437→                        std::string name;
  2438→                        uint8_t admin_level;
  2439→                        std::string country_code;
  2440→                    };
  2441→
  2442→                    std::vector<std::vector<AdminResult>> thread_admin_results(num_threads);
  2443→                    std::atomic<size_t> rel_idx{0};
  2444→                    std::atomic<uint64_t> assembled_count{0};

                            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 ---
                if (parallel_admin) {
                    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};


  2444→                    std::atomic<uint64_t> assembled_count{0};
  2445→
  2446→                    std::vector<std::thread> admin_workers;
  2447→                    for (unsigned int t = 0; t < num_threads; t++) {
  2448→                        admin_workers.emplace_back([&, t]() {
  2449→                            auto& local_results = thread_admin_results[t];
  2450→                            while (true) {
  2451→                                size_t i = rel_idx.fetch_add(1);
  2452→                                if (i >= data.collected_relations.size()) break;
  2453→
  2454→                                const auto& rel = data.collected_relations[i];
  2455→                                auto rings = assemble_outer_rings(rel.members, data.way_geometries);
  2456→
  2457→                                // Diagnostic: log relations with 0 rings produced
  2458→                                if (rings.empty() && !rel.members.empty()) {
  2459→                                    int total_outer = 0, found = 0, missing = 0;
  2460→                                    for (const auto& [way_id, role] : rel.members) {
  2461→                                        if (role == "outer" || role.empty()) {
  2462→                                            total_outer++;
  2463→                                            auto it = data.way_geometries.find(way_id);
  2464→                                            if (it != data.way_geometries.end() && !it->second.coords.empty()) {
  2465→                                                found++;
  2466→                                            } else {
  2467→                                                missing++;
  2468→                                            }
  2469→                                        }
  2470→                                    }
  2471→                                    if (missing == 0 && found > 0) {
  2472→                                        // All ways found but no ring — dump node IDs
  2473→                                        std::string detail = "  DEBUG '" + rel.name + "': ways:";

                    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];
                                auto rings = assemble_outer_rings(rel.members, data.way_geometries);

                                // Diagnostic: log relations with 0 rings produced
                                if (rings.empty() && !rel.members.empty()) {
                                    int total_outer = 0, found = 0, missing = 0;
                                    for (const auto& [way_id, role] : rel.members) {
                                        if (role == "outer" || role.empty()) {
                                            total_outer++;
                                            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) {
                                        // All ways found but no ring — dump node IDs
                                        std::string detail = "  DEBUG '" + rel.name + "': ways:";


I need to add the missing-ways check BEFORE calling assemble_outer_rings. If any outer member ways are missing, skip the relation.


Edit

                                const auto& rel = data.collected_relations[i];
                                auto rings = assemble_outer_rings(rel.members, data.way_geometries);

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

                                // Skip relations with missing outer 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 (role == "outer" || role.empty()) {
                                        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);