Bash

cat /root/traccar-geocoder/builder/src/build_index.cpp | ssh node3 "cat > /tmp/traccar-geocoder/builder/src/build_index.cpp" && ssh node3 "cd /tmp/traccar-geocoder/build && make -j\$(nproc) 2>&1 | tail -3 && rm -rf /tmp/de-par-rolefix4 && mkdir -p /tmp/de-par-rolefix4 && /tmp/traccar-geocoder/build/build-index /tmp/de-par-rolefix4 ~/pbf/germany.osm.pbf --parallel-admin 2>&1 | grep -E 'polygon|Done'" 2>&1

Build with no-outer-only fallback


[ 50%] Building CXX object CMakeFiles/build-index.dir/src/build_index.cpp.o
[100%] Linking CXX executable build-index
[100%] Built target build-index
  Added 37 admin polygons from closed ways.
  Assembling admin polygons in parallel (40080 relations, 112858 way geometries)...
  Parallel admin assembly complete: 43185 polygon rings from 40080 relations.
Done reading:
  43222 admin polygon rings
Waiting for admin polygon S2 covering to complete...
Admin polygon S2 covering complete (6103 cells).
admin index: 6103 cells, 43222 polygons
Done.


43,222 — same as before the role changes. The no-outer-only fallback doesn't trigger for Germany (La Mare etc. are Jersey/Albanian villages, not in Germany PBF).


ssh node3 "/tmp/freq_compare /tmp/de-par-rolefix4 /tmp/de-sequential-final" 2>&1

Verify Germany back to 3 diffs


  'Krefeld-Ost|9': A=2 B=1 diff=+1
  'Krefeld-Uerdingen|9': A=1 B=2 diff=-1
  'Neuhaus|10': A=5 B=4 diff=+1
Total frequency differences: 3


Good — Germany unchanged (3 diffs). Now let me run the Europe build to verify the 9 missing are fixed.


ssh node3 "mkdir -p ~/europe-parallel2 && nohup /tmp/traccar-geocoder/build/build-index ~/europe-parallel2 ~/pbf/europe.osm.pbf --parallel-admin > ~/europe-parallel2.log 2>&1 & echo PID: \$!" 2>&1

Start Europe build with role fix


Command running in background with ID: bu0m4g25p. Output is being written to: /tmp/claude-0/-root-traccar-geocoder/af1c4baa-0f9a-4420-a8b3-ce20dc28c8b9/tasks/bu0m4g25p.output


git add builder/src/build_index.cpp && git commit -m "$(cat <<'EOF'
fix: include inner-role ways and fix border-line filter for 9 missing admin polygons

Two fixes:

1. When outer-only ring assembly produces 0 rings AND the relation has
   NO outer/empty-role ways at all, retry with all member ways regardless
   of role. Osmium ignores roles during assembly (check_roles=false).
   This handles relations where the boundary is tagged as 'inner'
   (Jersey parishes, Albanian villages, Ukrainian borders).

2. Level-2 border-line filter now checks name pattern (" - " or " — ")
   instead of just missing ISO code. Fixes Akrotiri and Dhekelia (British
   sovereign territory with no ISO code that isn't a border line).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
EOF
)"

Commit role fix and border-line filter fix


[main 5cb0811] fix: include inner-role ways and fix border-line filter for 9 missing admin polygons
 1 file changed, 47 insertions(+), 25 deletions(-)


Now let me wait for the Europe build to complete.


sleep 1800 && ssh node3 "tail -5 ~/europe-parallel2.log; ps aux | grep 'build-index.*europe-parallel2' | grep -v grep | wc -l" 2>&1

Wait 30 min for Europe build


  Processing 19452480 street ways...
  Street ways done.
  Processing 13988 interpolation ways...
  Interpolation ways done.
  Merging thread-local results...
2


sleep 600 && ssh node3 "tail -5 ~/europe-parallel2.log; ps aux | grep 'build-index.*europe-parallel2' | grep -v grep | wc -l" 2>&1

Check Europe build (10 more min)


S2 cell computation complete.
Resolving interpolation endpoints...
Resolved 4187/13988 interpolation ways
Deduplicating...
Writing index files to /home/michtest/europe-parallel2...
2


Writing files — almost done.


sleep 300 && ssh node3 "tail -5 ~/europe-parallel2.log" 2>&1

Check completion


Writing index files to /home/michtest/europe-parallel2...
geo index: 86471638 cells (19452480 ways, 85891830 addrs, 13988 interps)
admin index: 141061 cells, 450902 polygons
strings.bin: 82546551 bytes
Done.


Done! 450,902 admin polygons (up from 450,896 — gained 6 from the fixes). Let me run the comparison.


ssh node3 "/tmp/dedup_compare ~/europe-parallel2 ~/europe-baseline" 2>&1

Deduplicated Europe comparison


A (/home/michtest/europe-parallel2): 327877 unique name|level
B (/home/michtest/europe-baseline): 327854 unique name|level
Both: 327850  Only_A: 27  Only_B: 4

Only in A (not in B):
  99750|11
  Aidu-Nõmme küla|9
  Albota de Jos|9
  Bibern|8
  Bogdanovca|9
  Butetown|10
  Bytków|9
  Carlyon|10
  Dilton Marsh|10
  Hilltown Great|10
  Hutton Cranswick|10
  Jakubskie Przedmieście|9
  Końcówek|9
  North Bradley|10
  Podolany|10

Only in B (not in A):
  Dardania|10
  Lakrishtë|10
  Qafa|10
  Qendra|10


Down from 9 missing to **4 missing**. Fixed 5 of the 9:
- Akrotiri and Dhekelia — fixed by border-line name pattern filter
- La Mare aux Bourguignons — fixed by inner-way fallback
- Théophile Le Tiec — fixed by inner-way fallback
- Березове — fixed by inner-way fallback
- Майдан-Чернелевецький — fixed by inner-way fallback

The remaining 4 (Dardania, Lakrishtë, Qafa, Qendra) are the Albanian villages that need inner ways to bridge gaps between outer ways. Our current fallback only triggers when there are **0 outer ways**. These relations have outer ways that don't close — we need to also retry when outer-only assembly fails even if outer ways exist.

But earlier that caused Reislingen to create a false polygon. The difference: the Albanian villages have inner ways that genuinely complete the ring (proper boundary topology), while Reislingen's inner way is a hole that creates a self-intersecting figure-8.

The fix: retry with all ways when outer-only fails, BUT check for self-intersections on the retry result.


Edit

                                if (rings.empty()) {
                                    // Retry with all ways only if the relation has
                                    // NO outer/empty-role ways at all (the boundary IS
                                    // the inner ways). Don't retry when outer ways exist
                                    // but fail to close — that's genuinely broken geometry.
                                    bool has_outer = false;
                                    for (const auto& [wid, role] : rel.members) {
                                        if (role == "outer" || role.empty()) {
                                            if (data.way_geometries.find(wid) != data.way_geometries.end()) {
                                                has_outer = true;
                                                break;
                                            }
                                        }
                                    }
                                    if (!has_outer) {
                                        rings = assemble_outer_rings(rel.members, data.way_geometries, true);
                                    }
                                }

                                if (rings.empty()) {
                                    // Retry with all ways (inner + outer) when outer-only fails.
                                    // Osmium ignores roles during assembly. Inner ways sometimes
                                    // form the actual boundary or bridge gaps between outer ways.
                                    rings = assemble_outer_rings(rel.members, data.way_geometries, true);
                                }