Now update the `simplify_polygon` call in `add_admin_polygon` — it previously took `500` as second arg, now uses the default from the header:


No matches found


339:// dp_simplify, simplify_polygon are in geometry.h
613:    auto simplified = simplify_polygon(vertices, 500);


Good — `simplify_polygon(vertices, 500)` still works since the header has a default parameter of `MAX_POLYGON_VERTICES = 500`. Now let's try to build and see if it compiles:


Bash

cd /root/traccar-geocoder/builder/build && cmake .. -DCMAKE_BUILD_TYPE=Release 2>&1 | tail -5 && make -j$(nproc) 2>&1 | tail -30

Build the project to check for compile errors


-- Configuring done (0.1s)
-- Generating done (0.0s)
-- Build files have been written to: /root/traccar-geocoder/builder/build
[ 50%] Building CXX object CMakeFiles/build-index.dir/src/build_index.cpp.o
[100%] Linking CXX executable build-index
[100%] Built target build-index


Builds cleanly. Now let me also verify the test file still compiles — it uses its own copies of some functions. Let me update it to use the headers:


     1→// Unit tests for ring assembly algorithms.
     2→// Tests the key edge cases that caused bugs during development:
     3→// - Coordinate-based endpoint matching (split/replaced nodes)
     4→// - Backtracking at branch points
     5→// - Closed-way admin polygon detection
     6→// - Inner-role way retry
     7→// - Self-intersection detection
     8→// - Border-line relation filtering
     9→// - Duplicate coordinate detection (hole merging)
    10→//
    11→// Build: g++ -O2 -std=c++17 -o test_ring_assembly test_ring_assembly.cpp
    12→// Run:   ./test_ring_assembly
    13→
    14→#include <algorithm>
    15→#include <cassert>
    16→#include <cmath>
    17→#include <cstdint>
    18→#include <iostream>
    19→#include <string>
    20→#include <unordered_map>
    21→#include <vector>
    22→
    23→// --- Minimal types matching build_index.cpp ---
    24→
    25→struct CellItemPair { uint64_t cell_id; uint32_t item_id; };
    26→
    27→namespace ParsedData {
    28→    struct WayGeometry {
    29→        std::vector<std::pair<double,double>> coords;
    30→        int64_t first_node_id;
    31→        int64_t last_node_id;
    32→    };
    33→}
    34→
    35→// Coordinate key matching osmium's Location (int32_t nanodegrees)

// Unit tests for ring assembly algorithms.
// Tests the key edge cases that caused bugs during development:
// - Coordinate-based endpoint matching (split/replaced nodes)
// - Backtracking at branch points
// - Closed-way admin polygon detection
// - Inner-role way retry
// - Self-intersection detection
// - Border-line relation filtering
// - Duplicate coordinate detection (hole merging)
//
// Build: g++ -O2 -std=c++17 -o test_ring_assembly test_ring_assembly.cpp
// Run:   ./test_ring_assembly

#include <algorithm>
#include <cassert>
#include <cmath>
#include <cstdint>
#include <iostream>
#include <string>
#include <unordered_map>
#include <vector>

// --- Minimal types matching build_index.cpp ---

struct CellItemPair { uint64_t cell_id; uint32_t item_id; };

namespace ParsedData {
    struct WayGeometry {
        std::vector<std::pair<double,double>> coords;
        int64_t first_node_id;
        int64_t last_node_id;
    };
}

// Coordinate key matching osmium's Location (int32_t nanodegrees)


I'll update the test file to include our new headers instead of duplicating definitions:


Edit

#include <algorithm>
#include <cassert>
#include <cmath>
#include <cstdint>
#include <iostream>
#include <string>
#include <unordered_map>
#include <vector>

// --- Minimal types matching build_index.cpp ---

struct CellItemPair { uint64_t cell_id; uint32_t item_id; };

namespace ParsedData {
    struct WayGeometry {
        std::vector<std::pair<double,double>> coords;
        int64_t first_node_id;
        int64_t last_node_id;
    };
}

// Coordinate key matching osmium's Location (int32_t nanodegrees)
static 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);
}

#include <algorithm>
#include <cassert>
#include <cmath>
#include <cstdint>
#include <cstring>
#include <iostream>
#include <string>
#include <unordered_map>
#include <vector>

#include "../src/types.h"
#include "../src/string_pool.h"
#include "../src/geometry.h"


The file /root/traccar-geocoder/builder/test/test_ring_assembly.cpp has been updated successfully.

#include <algorithm>
#include <cassert>
#include <cmath>
#include <cstdint>
#include <cstring>
#include <iostream>
#include <string>
#include <unordered_map>
#include <vector>

#include "../src/types.h"
#include "../src/string_pool.h"
#include "../src/geometry.h"

// Unit tests for ring assembly algorithms.
// Tests the key edge cases that caused bugs during development:
// - Coordinate-based endpoint matching (split/replaced nodes)
// - Backtracking at branch points
// - Closed-way admin polygon detection
// - Inner-role way retry
// - Self-intersection detection
// - Border-line relation filtering
// - Duplicate coordinate detection (hole merging)
//
// Build: g++ -O2 -std=c++17 -o test_ring_assembly test_ring_assembly.cpp
// Run:   ./test_ring_assembly

#include <algorithm>
#include <cassert>
#include <cmath>
#include <cstdint>
#include <iostream>
#include <string>
#include <unordered_map>
#include <vector>

// --- Minimal types matching build_index.cpp ---

struct CellItemPair { uint64_t cell_id; uint32_t item_id; };

namespace ParsedData {
    struct WayGeometry {
        std::vector<std::pair<double,double>> coords;
        int64_t first_node_id;
        int64_t last_node_id;
    };
}

// Coordinate key matching osmium's Location (int32_t nanodegrees)
static 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);
}

// --- Test helpers ---

static int tests_run = 0;
static int tests_passed = 0;

#define TEST(name) do { \
    tests_run++; \
    std::cerr << "  TEST: " << name << "... "; \
} while(0)

#define PASS() do { \
    tests_passed++; \
    std::cerr << "PASS" << std::endl; \
} while(0)

#define FAIL(msg) do { \
    std::cerr << "FAIL: " << msg << std::endl; \
} while(0)

template<typename T> std::string to_str(const T& v) { return std::to_string(v); }
inline std::string to_str(const std::string& v) { return "\"" + v + "\""; }
inline std::string to_str(const char* v) { return std::string("\"") + v + "\""; }

#define ASSERT_EQ(a, b) do { \
    if ((a) != (b)) { FAIL(#a " != " #b " (" + to_str(a) + " vs " + to_str(b) + ")"); return; } \
} while(0)

#define ASSERT_TRUE(cond) do { \
    if (!(cond)) { FAIL(#cond " is false"); return; } \
} while(0)

// --- Tests ---

void test_coord_key_basic() {
    TEST("coord_key basic");
    // Same coordinates should produce same key
    ASSERT_EQ(coord_key(51.5, -0.1), coord_key(51.5, -0.1));
    // Different coordinates should produce different keys
    ASSERT_TRUE(coord_key(51.5, -0.1) != coord_key(51.5, -0.2));
    ASSERT_TRUE(coord_key(51.5, -0.1) != coord_key(51.6, -0.1));
    PASS();
}

void test_coord_key_split_nodes() {
    TEST("coord_key handles split/replaced nodes at same location");
    // Two different OSM nodes at the same coordinates should have the same key
    // (even though they have different node IDs)
    double lat = 49.5576001, lng = 8.8074189;
    int64_t k1 = coord_key(lat, lng);
    int64_t k2 = coord_key(lat, lng);
    ASSERT_EQ(k1, k2);
    PASS();
}

void test_coord_key_negative_coords() {
    TEST("coord_key with negative coordinates");
    // Southern/western hemisphere
    int64_t k1 = coord_key(-33.8688, 151.2093); // Sydney
    int64_t k2 = coord_key(-33.8688, 151.2093);
    ASSERT_EQ(k1, k2);
    // Different cities
    ASSERT_TRUE(coord_key(-33.8688, 151.2093) != coord_key(-22.9068, -43.1729));
    PASS();
}

void test_self_intersection_simple_square() {
    TEST("self-intersection: simple square has no crossing");
    // A simple square should not self-intersect
    std::vector<std::pair<double,double>> square = {
        {0, 0}, {0, 1}, {1, 1}, {1, 0}, {0, 0}
    };
    // Simple brute-force check
    bool has_cross = false;
    size_t n = square.size();
    for (size_t i = 0; i + 1 < n && !has_cross; i++) {
        for (size_t j = i + 2; j + 1 < n && !has_cross; j++) {
            if (j == i + 1 || (i == 0 && j == n - 2)) continue;
            // Cross product intersection test
            auto cross = [](double ox, double oy, double ax, double ay, double bx, double by) {
                return (ax - ox) * (by - oy) - (ay - oy) * (bx - ox);
            };
            double d1 = cross(square[j].first, square[j].second, square[j+1].first, square[j+1].second, square[i].first, square[i].second);
            double d2 = cross(square[j].first, square[j].second, square[j+1].first, square[j+1].second, square[i+1].first, square[i+1].second);
            double d3 = cross(square[i].first, square[i].second, square[i+1].first, square[i+1].second, square[j].first, square[j].second);
            double d4 = cross(square[i].first, square[i].second, square[i+1].first, square[i+1].second, square[j+1].first, square[j+1].second);
            if (((d1 > 0 && d2 < 0) || (d1 < 0 && d2 > 0)) &&
                ((d3 > 0 && d4 < 0) || (d3 < 0 && d4 > 0))) {
                has_cross = true;
            }
        }
    }
    ASSERT_TRUE(!has_cross);
    PASS();
}

void test_self_intersection_bowtie() {
    TEST("self-intersection: bowtie (figure-8) detected");
    // A bowtie shape crosses itself at (0.5, 0.5)
    std::vector<std::pair<double,double>> bowtie = {
        {0, 0}, {1, 1}, {1, 0}, {0, 1}, {0, 0}
    };
    bool has_cross = false;
    size_t n = bowtie.size();
    for (size_t i = 0; i + 1 < n && !has_cross; i++) {
        for (size_t j = i + 2; j + 1 < n && !has_cross; j++) {
            if (j == i + 1 || (i == 0 && j == n - 2)) continue;
            auto cross = [](double ox, double oy, double ax, double ay, double bx, double by) {
                return (ax - ox) * (by - oy) - (ay - oy) * (bx - ox);
            };
            double d1 = cross(bowtie[j].first, bowtie[j].second, bowtie[j+1].first, bowtie[j+1].second, bowtie[i].first, bowtie[i].second);
            double d2 = cross(bowtie[j].first, bowtie[j].second, bowtie[j+1].first, bowtie[j+1].second, bowtie[i+1].first, bowtie[i+1].second);
            double d3 = cross(bowtie[i].first, bowtie[i].second, bowtie[i+1].first, bowtie[i+1].second, bowtie[j].first, bowtie[j].second);
            double d4 = cross(bowtie[i].first, bowtie[i].second, bowtie[i+1].first, bowtie[i+1].second, bowtie[j+1].first, bowtie[j+1].second);
            if (((d1 > 0 && d2 < 0) || (d1 < 0 && d2 > 0)) &&
                ((d3 > 0 && d4 < 0) || (d3 < 0 && d4 > 0))) {
                has_cross = true;
            }
        }
    }
    ASSERT_TRUE(has_cross);
    PASS();
}

void test_duplicate_coords_detection() {
    TEST("duplicate coordinate detection in ring");
    // A ring that visits a coordinate twice (figure-8 from merged hole)
    std::vector<std::pair<double,double>> ring = {
        {0, 0}, {1, 0}, {1, 1}, {0.5, 0.5}, {0, 1}, {0.5, 0.5}, {1, 1}, {0, 0}
    };
    // Check for duplicates
    std::unordered_map<int64_t, int> seen;
    bool has_dup = false;
    for (size_t i = 0; i + 1 < ring.size(); i++) {
        int64_t k = coord_key(ring[i].first, ring[i].second);
        if (++seen[k] > 1) { has_dup = true; break; }
    }
    ASSERT_TRUE(has_dup);
    PASS();
}

void test_no_duplicate_coords_simple_ring() {
    TEST("no duplicate coords in simple ring");
    std::vector<std::pair<double,double>> ring = {
        {0, 0}, {1, 0}, {1, 1}, {0, 1}, {0, 0}
    };
    std::unordered_map<int64_t, int> seen;
    bool has_dup = false;
    for (size_t i = 0; i + 1 < ring.size(); i++) {
        int64_t k = coord_key(ring[i].first, ring[i].second);
        if (++seen[k] > 1) { has_dup = true; break; }
    }
    ASSERT_TRUE(!has_dup);
    PASS();
}

void test_border_line_name_pattern() {
    TEST("border-line relation name detection");
    // Level 2 relations with " - " or " — " in name are border lines, not countries
    auto is_border_line = [](const std::string& name) {
        return name.find(" - ") != std::string::npos ||
               name.find(" \xe2\x80\x94 ") != std::string::npos; // em dash
    };
    ASSERT_TRUE(is_border_line("Deutschland - Österreich"));
    ASSERT_TRUE(is_border_line("France - Deutschland"));
    ASSERT_TRUE(is_border_line("Deutschland \xe2\x80\x94 Schweiz / Suisse / Svizerra"));
    ASSERT_TRUE(!is_border_line("Deutschland"));
    ASSERT_TRUE(!is_border_line("Akrotiri and Dhekelia")); // not a border line
    ASSERT_TRUE(!is_border_line("Österreich"));
    PASS();
}

void test_string_separator_bug() {
    TEST("string null separator (the \\0 bug)");
    // The original bug: std::string("x") + "\0" doesn't embed a null
    std::string bad = std::string("42") + "\0" + std::string("Main St");
    // This produces "42Main St" — the \0 is an empty string in the concatenation
    // The fix uses std::pair<std::string, std::string> instead

    // Verify the bug exists
    ASSERT_EQ(bad, std::string("42Main St")); // NOT "42\0Main St"
    ASSERT_EQ(bad.size(), 9u); // "42Main St" = 9 chars (no null embedded)

    // Verify the fix: pair stores them separately
    std::pair<std::string, std::string> fixed = {"42", "Main St"};
    ASSERT_EQ(fixed.first, "42");
    ASSERT_EQ(fixed.second, "Main St");
    PASS();
}

void test_cell_item_pair_sorting() {
    TEST("CellItemPair sorting by cell_id then item_id");
    std::vector<CellItemPair> pairs = {
        {100, 3}, {50, 1}, {100, 1}, {50, 2}, {100, 2}
    };
    std::sort(pairs.begin(), pairs.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);
    });
    ASSERT_EQ(pairs[0].cell_id, 50u);
    ASSERT_EQ(pairs[0].item_id, 1u);
    ASSERT_EQ(pairs[1].cell_id, 50u);
    ASSERT_EQ(pairs[1].item_id, 2u);
    ASSERT_EQ(pairs[2].cell_id, 100u);
    ASSERT_EQ(pairs[2].item_id, 1u);
    PASS();
}

void test_parallel_merge_join_correctness() {
    TEST("merge-join produces correct offsets");
    // Simulate: sorted_cells has cells 10,20,30,40,50
    // sorted_pairs has items for cells 10,30,50
    std::vector<uint64_t> sorted_cells = {10, 20, 30, 40, 50};
    std::vector<CellItemPair> sorted_pairs = {
        {10, 1}, {10, 2},
        {30, 3},
        {50, 4}, {50, 5}, {50, 6}
    };

    // Simulate merge-join
    const uint32_t NO_DATA = 0xFFFFFFFF;
    std::vector<uint32_t> offsets(sorted_cells.size(), NO_DATA);
    uint32_t current = 0;
    size_t pi = 0;
    for (uint32_t si = 0; si < sorted_cells.size() && pi < sorted_pairs.size(); si++) {
        if (sorted_cells[si] < sorted_pairs[pi].cell_id) continue;
        while (pi < sorted_pairs.size() && sorted_pairs[pi].cell_id < sorted_cells[si]) pi++;
        if (pi >= sorted_pairs.size() || sorted_pairs[pi].cell_id != sorted_cells[si]) continue;
        offsets[si] = current;
        size_t start = pi;
        while (pi < sorted_pairs.size() && sorted_pairs[pi].cell_id == sorted_cells[si]) pi++;
        current += sizeof(uint16_t) + (pi - start) * sizeof(uint32_t);
    }

    // Cell 10 (index 0) should have offset 0
    ASSERT_EQ(offsets[0], 0u);
    // Cell 20 (index 1) has no items
    ASSERT_EQ(offsets[1], NO_DATA);
    // Cell 30 (index 2) should have offset after cell 10's data
    uint32_t expected_30 = sizeof(uint16_t) + 2 * sizeof(uint32_t); // count(2) + 2 items
    ASSERT_EQ(offsets[2], expected_30);
    // Cell 40 (index 3) has no items
    ASSERT_EQ(offsets[3], NO_DATA);
    // Cell 50 (index 4) should have offset after cells 10+30
    uint32_t expected_50 = expected_30 + sizeof(uint16_t) + 1 * sizeof(uint32_t);
    ASSERT_EQ(offsets[4], expected_50);
    PASS();
}

void test_ring_area_computation() {
    TEST("ring area computation (shoelace formula)");
    // Unit square: area should be 1.0
    std::vector<std::pair<double,double>> square = {
        {0, 0}, {1, 0}, {1, 1}, {0, 1}, {0, 0}
    };
    double area = 0;
    for (size_t i = 0, n = square.size(); i < n; i++) {
        size_t j = (i + 1) % n;
        area += square[i].first * square[j].second;
        area -= square[j].first * square[i].second;
    }
    area = std::abs(area) / 2.0;
    ASSERT_TRUE(std::abs(area - 1.0) < 1e-10);
    PASS();
}

// --- Main ---

int main() {
    std::cerr << "Running ring assembly unit tests..." << std::endl;

    test_coord_key_basic();
    test_coord_key_split_nodes();
    test_coord_key_negative_coords();
    test_self_intersection_simple_square();
    test_self_intersection_bowtie();
    test_duplicate_coords_detection();
    test_no_duplicate_coords_simple_ring();
    test_border_line_name_pattern();
    test_string_separator_bug();
    test_cell_item_pair_sorting();
    test_parallel_merge_join_correctness();
    test_ring_area_computation();

    std::cerr << std::endl;
    std::cerr << tests_passed << "/" << tests_run << " tests passed." << std::endl;

    return (tests_passed == tests_run) ? 0 : 1;
}

-// --- Minimal types matching build_index.cpp ---

-struct CellItemPair { uint64_t cell_id; uint32_t item_id; };

-        std::vector<std::pair<double,double>> coords;

-// Coordinate key matching osmium's Location (int32_t nanodegrees)

-static int64_t coord_key(double lat, double lng) {

-    return (static_cast<int64_t>(ilat) << 32) | static_cast<uint32_t>(ilng);


Now update the tests that use manual self-intersection checks and duplicate coord checks to use the header functions:


    95→    // A simple square should not self-intersect
    96→    std::vector<std::pair<double,double>> square = {
    97→        {0, 0}, {0, 1}, {1, 1}, {1, 0}, {0, 0}
    98→    };
    99→    // Simple brute-force check
   100→    bool has_cross = false;
   101→    size_t n = square.size();
   102→    for (size_t i = 0; i + 1 < n && !has_cross; i++) {
   103→        for (size_t j = i + 2; j + 1 < n && !has_cross; j++) {
   104→            if (j == i + 1 || (i == 0 && j == n - 2)) continue;
   105→            // Cross product intersection test
   106→            auto cross = [](double ox, double oy, double ax, double ay, double bx, double by) {
   107→                return (ax - ox) * (by - oy) - (ay - oy) * (bx - ox);
   108→            };
   109→            double d1 = cross(square[j].first, square[j].second, square[j+1].first, square[j+1].second, square[i].first, square[i].second);
   110→            double d2 = cross(square[j].first, square[j].second, square[j+1].first, square[j+1].second, square[i+1].first, square[i+1].second);
   111→            double d3 = cross(square[i].first, square[i].second, square[i+1].first, square[i+1].second, square[j].first, square[j].second);
   112→            double d4 = cross(square[i].first, square[i].second, square[i+1].first, square[i+1].second, square[j+1].first, square[j+1].second);
   113→            if (((d1 > 0 && d2 < 0) || (d1 < 0 && d2 > 0)) &&
   114→                ((d3 > 0 && d4 < 0) || (d3 < 0 && d4 > 0))) {
   115→                has_cross = true;
   116→            }
   117→        }
   118→    }
   119→    ASSERT_TRUE(!has_cross);
   120→    PASS();
   121→}
   122→
   123→void test_self_intersection_bowtie() {
   124→    TEST("self-intersection: bowtie (figure-8) detected");
   125→    // A bowtie shape crosses itself at (0.5, 0.5)
   126→    std::vector<std::pair<double,double>> bowtie = {
   127→        {0, 0}, {1, 1}, {1, 0}, {0, 1}, {0, 0}
   128→    };
   129→    bool has_cross = false;
   130→    size_t n = bowtie.size();
   131→    for (size_t i = 0; i + 1 < n && !has_cross; i++) {
   132→        for (size_t j = i + 2; j + 1 < n && !has_cross; j++) {
   133→            if (j == i + 1 || (i == 0 && j == n - 2)) continue;
   134→            auto cross = [](double ox, double oy, double ax, double ay, double bx, double by) {
   135→                return (ax - ox) * (by - oy) - (ay - oy) * (bx - ox);
   136→            };
   137→            double d1 = cross(bowtie[j].first, bowtie[j].second, bowtie[j+1].first, bowtie[j+1].second, bowtie[i].first, bowtie[i].second);
   138→            double d2 = cross(bowtie[j].first, bowtie[j].second, bowtie[j+1].first, bowtie[j+1].second, bowtie[i+1].first, bowtie[i+1].second);
   139→            double d3 = cross(bowtie[i].first, bowtie[i].second, bowtie[i+1].first, bowtie[i+1].second, bowtie[j].first, bowtie[j].second);
   140→            double d4 = cross(bowtie[i].first, bowtie[i].second, bowtie[i+1].first, bowtie[i+1].second, bowtie[j+1].first, bowtie[j+1].second);
   141→            if (((d1 > 0 && d2 < 0) || (d1 < 0 && d2 > 0)) &&
   142→                ((d3 > 0 && d4 < 0) || (d3 < 0 && d4 > 0))) {
   143→                has_cross = true;
   144→            }
   145→        }
   146→    }
   147→    ASSERT_TRUE(has_cross);
   148→    PASS();
   149→}
   150→
   151→void test_duplicate_coords_detection() {
   152→    TEST("duplicate coordinate detection in ring");
   153→    // A ring that visits a coordinate twice (figure-8 from merged hole)
   154→    std::vector<std::pair<double,double>> ring = {
   155→        {0, 0}, {1, 0}, {1, 1}, {0.5, 0.5}, {0, 1}, {0.5, 0.5}, {1, 1}, {0, 0}
   156→    };
   157→    // Check for duplicates
   158→    std::unordered_map<int64_t, int> seen;
   159→    bool has_dup = false;
   160→    for (size_t i = 0; i + 1 < ring.size(); i++) {
   161→        int64_t k = coord_key(ring[i].first, ring[i].second);
   162→        if (++seen[k] > 1) { has_dup = true; break; }
   163→    }
   164→    ASSERT_TRUE(has_dup);
   165→    PASS();
   166→}
   167→
   168→void test_no_duplicate_coords_simple_ring() {
   169→    TEST("no duplicate coords in simple ring");
   170→    std::vector<std::pair<double,double>> ring = {
   171→        {0, 0}, {1, 0}, {1, 1}, {0, 1}, {0, 0}
   172→    };
   173→    std::unordered_map<int64_t, int> seen;
   174→    bool has_dup = false;
   175→    for (size_t i = 0; i + 1 < ring.size(); i++) {
   176→        int64_t k = coord_key(ring[i].first, ring[i].second);
   177→        if (++seen[k] > 1) { has_dup = true; break; }
   178→    }
   179→    ASSERT_TRUE(!has_dup);
   180→    PASS();
   181→}
   182→
   183→void test_border_line_name_pattern() {
   184→    TEST("border-line relation name detection");

    // A simple square should not self-intersect
    std::vector<std::pair<double,double>> square = {
        {0, 0}, {0, 1}, {1, 1}, {1, 0}, {0, 0}
    };
    // Simple brute-force check
    bool has_cross = false;
    size_t n = square.size();
    for (size_t i = 0; i + 1 < n && !has_cross; i++) {
        for (size_t j = i + 2; j + 1 < n && !has_cross; j++) {
            if (j == i + 1 || (i == 0 && j == n - 2)) continue;
            // Cross product intersection test
            auto cross = [](double ox, double oy, double ax, double ay, double bx, double by) {
                return (ax - ox) * (by - oy) - (ay - oy) * (bx - ox);
            };
            double d1 = cross(square[j].first, square[j].second, square[j+1].first, square[j+1].second, square[i].first, square[i].second);
            double d2 = cross(square[j].first, square[j].second, square[j+1].first, square[j+1].second, square[i+1].first, square[i+1].second);
            double d3 = cross(square[i].first, square[i].second, square[i+1].first, square[i+1].second, square[j].first, square[j].second);
            double d4 = cross(square[i].first, square[i].second, square[i+1].first, square[i+1].second, square[j+1].first, square[j+1].second);
            if (((d1 > 0 && d2 < 0) || (d1 < 0 && d2 > 0)) &&
                ((d3 > 0 && d4 < 0) || (d3 < 0 && d4 > 0))) {
                has_cross = true;
            }
        }
    }
    ASSERT_TRUE(!has_cross);
    PASS();
}

void test_self_intersection_bowtie() {
    TEST("self-intersection: bowtie (figure-8) detected");
    // A bowtie shape crosses itself at (0.5, 0.5)
    std::vector<std::pair<double,double>> bowtie = {
        {0, 0}, {1, 1}, {1, 0}, {0, 1}, {0, 0}
    };
    bool has_cross = false;
    size_t n = bowtie.size();
    for (size_t i = 0; i + 1 < n && !has_cross; i++) {
        for (size_t j = i + 2; j + 1 < n && !has_cross; j++) {
            if (j == i + 1 || (i == 0 && j == n - 2)) continue;
            auto cross = [](double ox, double oy, double ax, double ay, double bx, double by) {
                return (ax - ox) * (by - oy) - (ay - oy) * (bx - ox);
            };
            double d1 = cross(bowtie[j].first, bowtie[j].second, bowtie[j+1].first, bowtie[j+1].second, bowtie[i].first, bowtie[i].second);
            double d2 = cross(bowtie[j].first, bowtie[j].second, bowtie[j+1].first, bowtie[j+1].second, bowtie[i+1].first, bowtie[i+1].second);
            double d3 = cross(bowtie[i].first, bowtie[i].second, bowtie[i+1].first, bowtie[i+1].second, bowtie[j].first, bowtie[j].second);
            double d4 = cross(bowtie[i].first, bowtie[i].second, bowtie[i+1].first, bowtie[i+1].second, bowtie[j+1].first, bowtie[j+1].second);
            if (((d1 > 0 && d2 < 0) || (d1 < 0 && d2 > 0)) &&
                ((d3 > 0 && d4 < 0) || (d3 < 0 && d4 > 0))) {
                has_cross = true;
            }
        }
    }
    ASSERT_TRUE(has_cross);
    PASS();
}

void test_duplicate_coords_detection() {
    TEST("duplicate coordinate detection in ring");
    // A ring that visits a coordinate twice (figure-8 from merged hole)
    std::vector<std::pair<double,double>> ring = {
        {0, 0}, {1, 0}, {1, 1}, {0.5, 0.5}, {0, 1}, {0.5, 0.5}, {1, 1}, {0, 0}
    };
    // Check for duplicates
    std::unordered_map<int64_t, int> seen;
    bool has_dup = false;
    for (size_t i = 0; i + 1 < ring.size(); i++) {
        int64_t k = coord_key(ring[i].first, ring[i].second);
        if (++seen[k] > 1) { has_dup = true; break; }
    }
    ASSERT_TRUE(has_dup);
    PASS();
}

void test_no_duplicate_coords_simple_ring() {
    TEST("no duplicate coords in simple ring");
    std::vector<std::pair<double,double>> ring = {
        {0, 0}, {1, 0}, {1, 1}, {0, 1}, {0, 0}
    };
    std::unordered_map<int64_t, int> seen;
    bool has_dup = false;
    for (size_t i = 0; i + 1 < ring.size(); i++) {
        int64_t k = coord_key(ring[i].first, ring[i].second);
        if (++seen[k] > 1) { has_dup = true; break; }
    }
    ASSERT_TRUE(!has_dup);
    PASS();
}

void test_border_line_name_pattern() {
    TEST("border-line relation name detection");


Replace the manual intersection/duplicate tests with calls to the header functions:


void test_self_intersection_simple_square() {
    TEST("self-intersection: simple square has no crossing");
    // A simple square should not self-intersect
    std::vector<std::pair<double,double>> square = {
        {0, 0}, {0, 1}, {1, 1}, {1, 0}, {0, 0}
    };
    // Simple brute-force check
    bool has_cross = false;
    size_t n = square.size();
    for (size_t i = 0; i + 1 < n && !has_cross; i++) {
        for (size_t j = i + 2; j + 1 < n && !has_cross; j++) {
            if (j == i + 1 || (i == 0 && j == n - 2)) continue;
            // Cross product intersection test
            auto cross = [](double ox, double oy, double ax, double ay, double bx, double by) {
                return (ax - ox) * (by - oy) - (ay - oy) * (bx - ox);
            };
            double d1 = cross(square[j].first, square[j].second, square[j+1].first, square[j+1].second, square[i].first, square[i].second);
            double d2 = cross(square[j].first, square[j].second, square[j+1].first, square[j+1].second, square[i+1].first, square[i+1].second);
            double d3 = cross(square[i].first, square[i].second, square[i+1].first, square[i+1].second, square[j].first, square[j].second);
            double d4 = cross(square[i].first, square[i].second, square[i+1].first, square[i+1].second, square[j+1].first, square[j+1].second);
            if (((d1 > 0 && d2 < 0) || (d1 < 0 && d2 > 0)) &&
                ((d3 > 0 && d4 < 0) || (d3 < 0 && d4 > 0))) {
                has_cross = true;
            }
        }
    }
    ASSERT_TRUE(!has_cross);
    PASS();
}

void test_self_intersection_bowtie() {
    TEST("self-intersection: bowtie (figure-8) detected");
    // A bowtie shape crosses itself at (0.5, 0.5)
    std::vector<std::pair<double,double>> bowtie = {
        {0, 0}, {1, 1}, {1, 0}, {0, 1}, {0, 0}
    };
    bool has_cross = false;
    size_t n = bowtie.size();
    for (size_t i = 0; i + 1 < n && !has_cross; i++) {
        for (size_t j = i + 2; j + 1 < n && !has_cross; j++) {
            if (j == i + 1 || (i == 0 && j == n - 2)) continue;
            auto cross = [](double ox, double oy, double ax, double ay, double bx, double by) {
                return (ax - ox) * (by - oy) - (ay - oy) * (bx - ox);
            };
            double d1 = cross(bowtie[j].first, bowtie[j].second, bowtie[j+1].first, bowtie[j+1].second, bowtie[i].first, bowtie[i].second);
            double d2 = cross(bowtie[j].first, bowtie[j].second, bowtie[j+1].first, bowtie[j+1].second, bowtie[i+1].first, bowtie[i+1].second);
            double d3 = cross(bowtie[i].first, bowtie[i].second, bowtie[i+1].first, bowtie[i+1].second, bowtie[j].first, bowtie[j].second);
            double d4 = cross(bowtie[i].first, bowtie[i].second, bowtie[i+1].first, bowtie[i+1].second, bowtie[j+1].first, bowtie[j+1].second);
            if (((d1 > 0 && d2 < 0) || (d1 < 0 && d2 > 0)) &&
                ((d3 > 0 && d4 < 0) || (d3 < 0 && d4 > 0))) {
                has_cross = true;
            }
        }
    }
    ASSERT_TRUE(has_cross);
    PASS();
}

void test_duplicate_coords_detection() {
    TEST("duplicate coordinate detection in ring");
    // A ring that visits a coordinate twice (figure-8 from merged hole)
    std::vector<std::pair<double,double>> ring = {
        {0, 0}, {1, 0}, {1, 1}, {0.5, 0.5}, {0, 1}, {0.5, 0.5}, {1, 1}, {0, 0}
    };
    // Check for duplicates
    std::unordered_map<int64_t, int> seen;
    bool has_dup = false;
    for (size_t i = 0; i + 1 < ring.size(); i++) {
        int64_t k = coord_key(ring[i].first, ring[i].second);
        if (++seen[k] > 1) { has_dup = true; break; }
    }
    ASSERT_TRUE(has_dup);
    PASS();
}

void test_no_duplicate_coords_simple_ring() {
    TEST("no duplicate coords in simple ring");
    std::vector<std::pair<double,double>> ring = {
        {0, 0}, {1, 0}, {1, 1}, {0, 1}, {0, 0}
    };
    std::unordered_map<int64_t, int> seen;
    bool has_dup = false;
    for (size_t i = 0; i + 1 < ring.size(); i++) {
        int64_t k = coord_key(ring[i].first, ring[i].second);
        if (++seen[k] > 1) { has_dup = true; break; }
    }
    ASSERT_TRUE(!has_dup);
    PASS();
}

void test_self_intersection_simple_square() {
    TEST("self-intersection: simple square has no crossing");
    std::vector<std::pair<double,double>> square = {
        {0, 0}, {0, 1}, {1, 1}, {1, 0}, {0, 0}
    };
    ASSERT_TRUE(!ring_has_self_intersection(square));
    PASS();
}

void test_self_intersection_bowtie() {
    TEST("self-intersection: bowtie (figure-8) detected");
    std::vector<std::pair<double,double>> bowtie = {
        {0, 0}, {1, 1}, {1, 0}, {0, 1}, {0, 0}
    };
    ASSERT_TRUE(ring_has_self_intersection(bowtie));
    PASS();
}

void test_duplicate_coords_detection() {
    TEST("duplicate coordinate detection in ring");
    std::vector<std::pair<double,double>> ring = {
        {0, 0}, {1, 0}, {1, 1}, {0.5, 0.5}, {0, 1}, {0.5, 0.5}, {1, 1}, {0, 0}
    };
    ASSERT_TRUE(ring_has_duplicate_coords(ring));
    PASS();
}

void test_no_duplicate_coords_simple_ring() {
    TEST("no duplicate coords in simple ring");
    std::vector<std::pair<double,double>> ring = {
        {0, 0}, {1, 0}, {1, 1}, {0, 1}, {0, 0}
    };
    ASSERT_TRUE(!ring_has_duplicate_coords(ring));
    PASS();
}


void test_self_intersection_simple_square() {
    TEST("self-intersection: simple square has no crossing");
    std::vector<std::pair<double,double>> square = {
        {0, 0}, {0, 1}, {1, 1}, {1, 0}, {0, 0}
    };
    ASSERT_TRUE(!ring_has_self_intersection(square));
    PASS();
}

void test_self_intersection_bowtie() {
    TEST("self-intersection: bowtie (figure-8) detected");
    std::vector<std::pair<double,double>> bowtie = {
        {0, 0}, {1, 1}, {1, 0}, {0, 1}, {0, 0}
    };
    ASSERT_TRUE(ring_has_self_intersection(bowtie));
    PASS();
}

void test_duplicate_coords_detection() {
    TEST("duplicate coordinate detection in ring");
    std::vector<std::pair<double,double>> ring = {
        {0, 0}, {1, 0}, {1, 1}, {0.5, 0.5}, {0, 1}, {0.5, 0.5}, {1, 1}, {0, 0}
    };
    ASSERT_TRUE(ring_has_duplicate_coords(ring));
    PASS();
}

void test_no_duplicate_coords_simple_ring() {
    TEST("no duplicate coords in simple ring");
    std::vector<std::pair<double,double>> ring = {
        {0, 0}, {1, 0}, {1, 1}, {0, 1}, {0, 0}
    };
    ASSERT_TRUE(!ring_has_duplicate_coords(ring));
    PASS();
}

// Unit tests for ring assembly algorithms.
// Tests the key edge cases that caused bugs during development:
// - Coordinate-based endpoint matching (split/replaced nodes)
// - Backtracking at branch points
// - Closed-way admin polygon detection
// - Inner-role way retry
// - Self-intersection detection
// - Border-line relation filtering
// - Duplicate coordinate detection (hole merging)
//
// Build: g++ -O2 -std=c++17 -o test_ring_assembly test_ring_assembly.cpp
// Run:   ./test_ring_assembly

#include <algorithm>
#include <cassert>
#include <cmath>
#include <cstdint>
#include <cstring>
#include <iostream>
#include <string>
#include <unordered_map>
#include <vector>

#include "../src/types.h"
#include "../src/string_pool.h"
#include "../src/geometry.h"

// --- Test helpers ---

static int tests_run = 0;
static int tests_passed = 0;

#define TEST(name) do { \
    tests_run++; \
    std::cerr << "  TEST: " << name << "... "; \
} while(0)

#define PASS() do { \
    tests_passed++; \
    std::cerr << "PASS" << std::endl; \
} while(0)

#define FAIL(msg) do { \
    std::cerr << "FAIL: " << msg << std::endl; \
} while(0)

template<typename T> std::string to_str(const T& v) { return std::to_string(v); }
inline std::string to_str(const std::string& v) { return "\"" + v + "\""; }
inline std::string to_str(const char* v) { return std::string("\"") + v + "\""; }

#define ASSERT_EQ(a, b) do { \
    if ((a) != (b)) { FAIL(#a " != " #b " (" + to_str(a) + " vs " + to_str(b) + ")"); return; } \
} while(0)

#define ASSERT_TRUE(cond) do { \
    if (!(cond)) { FAIL(#cond " is false"); return; } \
} while(0)

// --- Tests ---

void test_coord_key_basic() {
    TEST("coord_key basic");
    // Same coordinates should produce same key
    ASSERT_EQ(coord_key(51.5, -0.1), coord_key(51.5, -0.1));
    // Different coordinates should produce different keys
    ASSERT_TRUE(coord_key(51.5, -0.1) != coord_key(51.5, -0.2));
    ASSERT_TRUE(coord_key(51.5, -0.1) != coord_key(51.6, -0.1));
    PASS();
}

void test_coord_key_split_nodes() {
    TEST("coord_key handles split/replaced nodes at same location");
    // Two different OSM nodes at the same coordinates should have the same key
    // (even though they have different node IDs)
    double lat = 49.5576001, lng = 8.8074189;
    int64_t k1 = coord_key(lat, lng);
    int64_t k2 = coord_key(lat, lng);
    ASSERT_EQ(k1, k2);
    PASS();
}

void test_coord_key_negative_coords() {
    TEST("coord_key with negative coordinates");
    // Southern/western hemisphere
    int64_t k1 = coord_key(-33.8688, 151.2093); // Sydney
    int64_t k2 = coord_key(-33.8688, 151.2093);
    ASSERT_EQ(k1, k2);
    // Different cities
    ASSERT_TRUE(coord_key(-33.8688, 151.2093) != coord_key(-22.9068, -43.1729));
    PASS();
}

void test_self_intersection_simple_square() {
    TEST("self-intersection: simple square has no crossing");
    // A simple square should not self-intersect
    std::vector<std::pair<double,double>> square = {
        {0, 0}, {0, 1}, {1, 1}, {1, 0}, {0, 0}
    };
    // Simple brute-force check
    bool has_cross = false;
    size_t n = square.size();
    for (size_t i = 0; i + 1 < n && !has_cross; i++) {
        for (size_t j = i + 2; j + 1 < n && !has_cross; j++) {
            if (j == i + 1 || (i == 0 && j == n - 2)) continue;
            // Cross product intersection test
            auto cross = [](double ox, double oy, double ax, double ay, double bx, double by) {
                return (ax - ox) * (by - oy) - (ay - oy) * (bx - ox);
            };
            double d1 = cross(square[j].first, square[j].second, square[j+1].first, square[j+1].second, square[i].first, square[i].second);
            double d2 = cross(square[j].first, square[j].second, square[j+1].first, square[j+1].second, square[i+1].first, square[i+1].second);
            double d3 = cross(square[i].first, square[i].second, square[i+1].first, square[i+1].second, square[j].first, square[j].second);
            double d4 = cross(square[i].first, square[i].second, square[i+1].first, square[i+1].second, square[j+1].first, square[j+1].second);
            if (((d1 > 0 && d2 < 0) || (d1 < 0 && d2 > 0)) &&
                ((d3 > 0 && d4 < 0) || (d3 < 0 && d4 > 0))) {
                has_cross = true;
            }
        }
    }
    ASSERT_TRUE(!has_cross);
    PASS();
}

void test_self_intersection_bowtie() {
    TEST("self-intersection: bowtie (figure-8) detected");
    // A bowtie shape crosses itself at (0.5, 0.5)
    std::vector<std::pair<double,double>> bowtie = {
        {0, 0}, {1, 1}, {1, 0}, {0, 1}, {0, 0}
    };
    bool has_cross = false;
    size_t n = bowtie.size();
    for (size_t i = 0; i + 1 < n && !has_cross; i++) {
        for (size_t j = i + 2; j + 1 < n && !has_cross; j++) {
            if (j == i + 1 || (i == 0 && j == n - 2)) continue;
            auto cross = [](double ox, double oy, double ax, double ay, double bx, double by) {
                return (ax - ox) * (by - oy) - (ay - oy) * (bx - ox);
            };
            double d1 = cross(bowtie[j].first, bowtie[j].second, bowtie[j+1].first, bowtie[j+1].second, bowtie[i].first, bowtie[i].second);
            double d2 = cross(bowtie[j].first, bowtie[j].second, bowtie[j+1].first, bowtie[j+1].second, bowtie[i+1].first, bowtie[i+1].second);
            double d3 = cross(bowtie[i].first, bowtie[i].second, bowtie[i+1].first, bowtie[i+1].second, bowtie[j].first, bowtie[j].second);
            double d4 = cross(bowtie[i].first, bowtie[i].second, bowtie[i+1].first, bowtie[i+1].second, bowtie[j+1].first, bowtie[j+1].second);
            if (((d1 > 0 && d2 < 0) || (d1 < 0 && d2 > 0)) &&
                ((d3 > 0 && d4 < 0) || (d3 < 0 && d4 > 0))) {
                has_cross = true;
            }
        }
    }
    ASSERT_TRUE(has_cross);
    PASS();
}

void test_duplicate_coords_detection() {
    TEST("duplicate coordinate detection in ring");
    // A ring that visits a coordinate twice (figure-8 from merged hole)
    std::vector<std::pair<double,double>> ring = {
        {0, 0}, {1, 0}, {1, 1}, {0.5, 0.5}, {0, 1}, {0.5, 0.5}, {1, 1}, {0, 0}
    };
    // Check for duplicates
    std::unordered_map<int64_t, int> seen;
    bool has_dup = false;
    for (size_t i = 0; i + 1 < ring.size(); i++) {
        int64_t k = coord_key(ring[i].first, ring[i].second);
        if (++seen[k] > 1) { has_dup = true; break; }
    }
    ASSERT_TRUE(has_dup);
    PASS();
}

void test_no_duplicate_coords_simple_ring() {
    TEST("no duplicate coords in simple ring");
    std::vector<std::pair<double,double>> ring = {
        {0, 0}, {1, 0}, {1, 1}, {0, 1}, {0, 0}
    };
    std::unordered_map<int64_t, int> seen;
    bool has_dup = false;
    for (size_t i = 0; i + 1 < ring.size(); i++) {
        int64_t k = coord_key(ring[i].first, ring[i].second);
        if (++seen[k] > 1) { has_dup = true; break; }
    }
    ASSERT_TRUE(!has_dup);
    PASS();
}

void test_border_line_name_pattern() {
    TEST("border-line relation name detection");
    // Level 2 relations with " - " or " — " in name are border lines, not countries
    auto is_border_line = [](const std::string& name) {
        return name.find(" - ") != std::string::npos ||
               name.find(" \xe2\x80\x94 ") != std::string::npos; // em dash
    };
    ASSERT_TRUE(is_border_line("Deutschland - Österreich"));
    ASSERT_TRUE(is_border_line("France - Deutschland"));
    ASSERT_TRUE(is_border_line("Deutschland \xe2\x80\x94 Schweiz / Suisse / Svizerra"));
    ASSERT_TRUE(!is_border_line("Deutschland"));
    ASSERT_TRUE(!is_border_line("Akrotiri and Dhekelia")); // not a border line
    ASSERT_TRUE(!is_border_line("Österreich"));
    PASS();
}

void test_string_separator_bug() {
    TEST("string null separator (the \\0 bug)");
    // The original bug: std::string("x") + "\0" doesn't embed a null
    std::string bad = std::string("42") + "\0" + std::string("Main St");
    // This produces "42Main St" — the \0 is an empty string in the concatenation
    // The fix uses std::pair<std::string, std::string> instead

    // Verify the bug exists
    ASSERT_EQ(bad, std::string("42Main St")); // NOT "42\0Main St"
    ASSERT_EQ(bad.size(), 9u); // "42Main St" = 9 chars (no null embedded)

    // Verify the fix: pair stores them separately
    std::pair<std::string, std::string> fixed = {"42", "Main St"};
    ASSERT_EQ(fixed.first, "42");
    ASSERT_EQ(fixed.second, "Main St");
    PASS();
}

void test_cell_item_pair_sorting() {
    TEST("CellItemPair sorting by cell_id then item_id");
    std::vector<CellItemPair> pairs = {
        {100, 3}, {50, 1}, {100, 1}, {50, 2}, {100, 2}
    };
    std::sort(pairs.begin(), pairs.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);
    });
    ASSERT_EQ(pairs[0].cell_id, 50u);
    ASSERT_EQ(pairs[0].item_id, 1u);
    ASSERT_EQ(pairs[1].cell_id, 50u);
    ASSERT_EQ(pairs[1].item_id, 2u);
    ASSERT_EQ(pairs[2].cell_id, 100u);
    ASSERT_EQ(pairs[2].item_id, 1u);
    PASS();
}

void test_parallel_merge_join_correctness() {
    TEST("merge-join produces correct offsets");
    // Simulate: sorted_cells has cells 10,20,30,40,50
    // sorted_pairs has items for cells 10,30,50
    std::vector<uint64_t> sorted_cells = {10, 20, 30, 40, 50};
    std::vector<CellItemPair> sorted_pairs = {
        {10, 1}, {10, 2},
        {30, 3},
        {50, 4}, {50, 5}, {50, 6}
    };

    // Simulate merge-join
    const uint32_t NO_DATA = 0xFFFFFFFF;
    std::vector<uint32_t> offsets(sorted_cells.size(), NO_DATA);
    uint32_t current = 0;
    size_t pi = 0;
    for (uint32_t si = 0; si < sorted_cells.size() && pi < sorted_pairs.size(); si++) {
        if (sorted_cells[si] < sorted_pairs[pi].cell_id) continue;
        while (pi < sorted_pairs.size() && sorted_pairs[pi].cell_id < sorted_cells[si]) pi++;
        if (pi >= sorted_pairs.size() || sorted_pairs[pi].cell_id != sorted_cells[si]) continue;
        offsets[si] = current;
        size_t start = pi;
        while (pi < sorted_pairs.size() && sorted_pairs[pi].cell_id == sorted_cells[si]) pi++;
        current += sizeof(uint16_t) + (pi - start) * sizeof(uint32_t);
    }

    // Cell 10 (index 0) should have offset 0
    ASSERT_EQ(offsets[0], 0u);
    // Cell 20 (index 1) has no items
    ASSERT_EQ(offsets[1], NO_DATA);
    // Cell 30 (index 2) should have offset after cell 10's data
    uint32_t expected_30 = sizeof(uint16_t) + 2 * sizeof(uint32_t); // count(2) + 2 items
    ASSERT_EQ(offsets[2], expected_30);
    // Cell 40 (index 3) has no items
    ASSERT_EQ(offsets[3], NO_DATA);
    // Cell 50 (index 4) should have offset after cells 10+30
    uint32_t expected_50 = expected_30 + sizeof(uint16_t) + 1 * sizeof(uint32_t);
    ASSERT_EQ(offsets[4], expected_50);
    PASS();
}

void test_ring_area_computation() {
    TEST("ring area computation (shoelace formula)");
    // Unit square: area should be 1.0
    std::vector<std::pair<double,double>> square = {
        {0, 0}, {1, 0}, {1, 1}, {0, 1}, {0, 0}
    };
    double area = 0;
    for (size_t i = 0, n = square.size(); i < n; i++) {
        size_t j = (i + 1) % n;
        area += square[i].first * square[j].second;
        area -= square[j].first * square[i].second;
    }
    area = std::abs(area) / 2.0;
    ASSERT_TRUE(std::abs(area - 1.0) < 1e-10);
    PASS();
}

// --- Main ---

int main() {
    std::cerr << "Running ring assembly unit tests..." << std::endl;

    test_coord_key_basic();
    test_coord_key_split_nodes();
    test_coord_key_negative_coords();
    test_self_intersection_simple_square();
    test_self_intersection_bowtie();
    test_duplicate_coords_detection();
    test_no_duplicate_coords_simple_ring();
    test_border_line_name_pattern();
    test_string_separator_bug();
    test_cell_item_pair_sorting();
    test_parallel_merge_join_correctness();
    test_ring_area_computation();

    std::cerr << std::endl;
    std::cerr << tests_passed << "/" << tests_run << " tests passed." << std::endl;

    return (tests_passed == tests_run) ? 0 : 1;
}

-    // A simple square should not self-intersect

-    for (size_t i = 0; i + 1 < n && !has_cross; i++) {

-        for (size_t j = i + 2; j + 1 < n && !has_cross; j++) {

-            // Cross product intersection test

-            auto cross = [](double ox, double oy, double ax, double ay, double bx, double by) {

-                return (ax - ox) * (by - oy) - (ay - oy) * (bx - ox);

-            double d1 = cross(square[j].first, square[j].second, square[j+1].first, square[j+1].second, square[i].first, square[i].second);

-            double d2 = cross(square[j].first, square[j].second, square[j+1].first, square[j+1].second, square[i+1].first, square[i+1].second);

-            double d3 = cross(square[i].first, square[i].second, square[i+1].first, square[i+1].second, square[j].first, square[j].second);

-            double d4 = cross(square[i].first, square[i].second, square[i+1].first, square[i+1].second, square[j+1].first, square[j+1].second);

+    ASSERT_TRUE(!ring_has_self_intersection(square));

-    // A bowtie shape crosses itself at (0.5, 0.5)

-            double d1 = cross(bowtie[j].first, bowtie[j].second, bowtie[j+1].first, bowtie[j+1].second, bowtie[i].first, bowtie[i].second);

-            double d2 = cross(bowtie[j].first, bowtie[j].second, bowtie[j+1].first, bowtie[j+1].second, bowtie[i+1].first, bowtie[i+1].second);

-            double d3 = cross(bowtie[i].first, bowtie[i].second, bowtie[i+1].first, bowtie[i+1].second, bowtie[j].first, bowtie[j].second);

-            double d4 = cross(bowtie[i].first, bowtie[i].second, bowtie[i+1].first, bowtie[i+1].second, bowtie[j+1].first, bowtie[j+1].second);

+    ASSERT_TRUE(ring_has_self_intersection(bowtie));

-    // A ring that visits a coordinate twice (figure-8 from merged hole)

-    std::unordered_map<int64_t, int> seen;

-    for (size_t i = 0; i + 1 < ring.size(); i++) {

-        int64_t k = coord_key(ring[i].first, ring[i].second);

-        if (++seen[k] > 1) { has_dup = true; break; }

+    ASSERT_TRUE(ring_has_duplicate_coords(ring));

+    ASSERT_TRUE(!ring_has_duplicate_coords(ring));


Now update the ring area test to use the header function:


   220→
   221→void test_ring_area_computation() {
   222→    TEST("ring area computation (shoelace formula)");
   223→    // Unit square: area should be 1.0
   224→    std::vector<std::pair<double,double>> square = {
   225→        {0, 0}, {1, 0}, {1, 1}, {0, 1}, {0, 0}
   226→    };
   227→    double area = 0;
   228→    for (size_t i = 0, n = square.size(); i < n; i++) {
   229→        size_t j = (i + 1) % n;
   230→        area += square[i].first * square[j].second;
   231→        area -= square[j].first * square[i].second;
   232→    }
   233→    area = std::abs(area) / 2.0;
   234→    ASSERT_TRUE(std::abs(area - 1.0) < 1e-10);
   235→    PASS();
   236→}
   237→
   238→// --- Main ---
   239→
   240→int main() {
   241→    std::cerr << "Running ring assembly unit tests..." << std::endl;
   242→
   243→    test_coord_key_basic();
   244→    test_coord_key_split_nodes();
   245→    test_coord_key_negative_coords();
   246→    test_self_intersection_simple_square();
   247→    test_self_intersection_bowtie();
   248→    test_duplicate_coords_detection();
   249→    test_no_duplicate_coords_simple_ring();

void test_ring_area_computation() {
    TEST("ring area computation (shoelace formula)");
    // Unit square: area should be 1.0
    std::vector<std::pair<double,double>> square = {
        {0, 0}, {1, 0}, {1, 1}, {0, 1}, {0, 0}
    };
    double area = 0;
    for (size_t i = 0, n = square.size(); i < n; i++) {
        size_t j = (i + 1) % n;
        area += square[i].first * square[j].second;
        area -= square[j].first * square[i].second;
    }
    area = std::abs(area) / 2.0;
    ASSERT_TRUE(std::abs(area - 1.0) < 1e-10);
    PASS();
}

// --- Main ---

int main() {
    std::cerr << "Running ring assembly unit tests..." << std::endl;

    test_coord_key_basic();
    test_coord_key_split_nodes();
    test_coord_key_negative_coords();
    test_self_intersection_simple_square();
    test_self_intersection_bowtie();
    test_duplicate_coords_detection();
    test_no_duplicate_coords_simple_ring();


void test_ring_area_computation() {
    TEST("ring area computation (shoelace formula)");
    // Unit square: area should be 1.0
    std::vector<std::pair<double,double>> square = {
        {0, 0}, {1, 0}, {1, 1}, {0, 1}, {0, 0}
    };
    double area = 0;
    for (size_t i = 0, n = square.size(); i < n; i++) {
        size_t j = (i + 1) % n;
        area += square[i].first * square[j].second;
        area -= square[j].first * square[i].second;
    }
    area = std::abs(area) / 2.0;
    ASSERT_TRUE(std::abs(area - 1.0) < 1e-10);
    PASS();
}

void test_ring_area_computation() {
    TEST("ring area computation (shoelace formula)");
    std::vector<std::pair<double,double>> square = {
        {0, 0}, {1, 0}, {1, 1}, {0, 1}, {0, 0}
    };
    float area = polygon_area(square);
    ASSERT_TRUE(std::abs(area - 1.0f) < 1e-6f);
    PASS();
}


void test_ring_area_computation() {
    TEST("ring area computation (shoelace formula)");
    std::vector<std::pair<double,double>> square = {
        {0, 0}, {1, 0}, {1, 1}, {0, 1}, {0, 0}
    };
    float area = polygon_area(square);
    ASSERT_TRUE(std::abs(area - 1.0f) < 1e-6f);
    PASS();
}

// Unit tests for ring assembly algorithms.
// Tests the key edge cases that caused bugs during development:
// - Coordinate-based endpoint matching (split/replaced nodes)
// - Backtracking at branch points
// - Closed-way admin polygon detection
// - Inner-role way retry
// - Self-intersection detection
// - Border-line relation filtering
// - Duplicate coordinate detection (hole merging)
//
// Build: g++ -O2 -std=c++17 -o test_ring_assembly test_ring_assembly.cpp
// Run:   ./test_ring_assembly

#include <algorithm>
#include <cassert>
#include <cmath>
#include <cstdint>
#include <cstring>
#include <iostream>
#include <string>
#include <unordered_map>
#include <vector>

#include "../src/types.h"
#include "../src/string_pool.h"
#include "../src/geometry.h"

// --- Test helpers ---

static int tests_run = 0;
static int tests_passed = 0;

#define TEST(name) do { \
    tests_run++; \
    std::cerr << "  TEST: " << name << "... "; \
} while(0)

#define PASS() do { \
    tests_passed++; \
    std::cerr << "PASS" << std::endl; \
} while(0)

#define FAIL(msg) do { \
    std::cerr << "FAIL: " << msg << std::endl; \
} while(0)

template<typename T> std::string to_str(const T& v) { return std::to_string(v); }
inline std::string to_str(const std::string& v) { return "\"" + v + "\""; }
inline std::string to_str(const char* v) { return std::string("\"") + v + "\""; }

#define ASSERT_EQ(a, b) do { \
    if ((a) != (b)) { FAIL(#a " != " #b " (" + to_str(a) + " vs " + to_str(b) + ")"); return; } \
} while(0)

#define ASSERT_TRUE(cond) do { \
    if (!(cond)) { FAIL(#cond " is false"); return; } \
} while(0)

// --- Tests ---

void test_coord_key_basic() {
    TEST("coord_key basic");
    // Same coordinates should produce same key
    ASSERT_EQ(coord_key(51.5, -0.1), coord_key(51.5, -0.1));
    // Different coordinates should produce different keys
    ASSERT_TRUE(coord_key(51.5, -0.1) != coord_key(51.5, -0.2));
    ASSERT_TRUE(coord_key(51.5, -0.1) != coord_key(51.6, -0.1));
    PASS();
}

void test_coord_key_split_nodes() {
    TEST("coord_key handles split/replaced nodes at same location");
    // Two different OSM nodes at the same coordinates should have the same key
    // (even though they have different node IDs)
    double lat = 49.5576001, lng = 8.8074189;
    int64_t k1 = coord_key(lat, lng);
    int64_t k2 = coord_key(lat, lng);
    ASSERT_EQ(k1, k2);
    PASS();
}

void test_coord_key_negative_coords() {
    TEST("coord_key with negative coordinates");
    // Southern/western hemisphere
    int64_t k1 = coord_key(-33.8688, 151.2093); // Sydney
    int64_t k2 = coord_key(-33.8688, 151.2093);
    ASSERT_EQ(k1, k2);
    // Different cities
    ASSERT_TRUE(coord_key(-33.8688, 151.2093) != coord_key(-22.9068, -43.1729));
    PASS();
}

void test_self_intersection_simple_square() {
    TEST("self-intersection: simple square has no crossing");
    std::vector<std::pair<double,double>> square = {
        {0, 0}, {0, 1}, {1, 1}, {1, 0}, {0, 0}
    };
    ASSERT_TRUE(!ring_has_self_intersection(square));
    PASS();
}

void test_self_intersection_bowtie() {
    TEST("self-intersection: bowtie (figure-8) detected");
    std::vector<std::pair<double,double>> bowtie = {
        {0, 0}, {1, 1}, {1, 0}, {0, 1}, {0, 0}
    };
    ASSERT_TRUE(ring_has_self_intersection(bowtie));
    PASS();
}

void test_duplicate_coords_detection() {
    TEST("duplicate coordinate detection in ring");
    std::vector<std::pair<double,double>> ring = {
        {0, 0}, {1, 0}, {1, 1}, {0.5, 0.5}, {0, 1}, {0.5, 0.5}, {1, 1}, {0, 0}
    };
    ASSERT_TRUE(ring_has_duplicate_coords(ring));
    PASS();
}

void test_no_duplicate_coords_simple_ring() {
    TEST("no duplicate coords in simple ring");
    std::vector<std::pair<double,double>> ring = {
        {0, 0}, {1, 0}, {1, 1}, {0, 1}, {0, 0}
    };
    ASSERT_TRUE(!ring_has_duplicate_coords(ring));
    PASS();
}

void test_border_line_name_pattern() {
    TEST("border-line relation name detection");
    // Level 2 relations with " - " or " — " in name are border lines, not countries
    auto is_border_line = [](const std::string& name) {
        return name.find(" - ") != std::string::npos ||
               name.find(" \xe2\x80\x94 ") != std::string::npos; // em dash
    };
    ASSERT_TRUE(is_border_line("Deutschland - Österreich"));
    ASSERT_TRUE(is_border_line("France - Deutschland"));
    ASSERT_TRUE(is_border_line("Deutschland \xe2\x80\x94 Schweiz / Suisse / Svizerra"));
    ASSERT_TRUE(!is_border_line("Deutschland"));
    ASSERT_TRUE(!is_border_line("Akrotiri and Dhekelia")); // not a border line
    ASSERT_TRUE(!is_border_line("Österreich"));
    PASS();
}

void test_string_separator_bug() {
    TEST("string null separator (the \\0 bug)");
    // The original bug: std::string("x") + "\0" doesn't embed a null
    std::string bad = std::string("42") + "\0" + std::string("Main St");
    // This produces "42Main St" — the \0 is an empty string in the concatenation
    // The fix uses std::pair<std::string, std::string> instead

    // Verify the bug exists
    ASSERT_EQ(bad, std::string("42Main St")); // NOT "42\0Main St"
    ASSERT_EQ(bad.size(), 9u); // "42Main St" = 9 chars (no null embedded)

    // Verify the fix: pair stores them separately
    std::pair<std::string, std::string> fixed = {"42", "Main St"};
    ASSERT_EQ(fixed.first, "42");
    ASSERT_EQ(fixed.second, "Main St");
    PASS();
}

void test_cell_item_pair_sorting() {
    TEST("CellItemPair sorting by cell_id then item_id");
    std::vector<CellItemPair> pairs = {
        {100, 3}, {50, 1}, {100, 1}, {50, 2}, {100, 2}
    };
    std::sort(pairs.begin(), pairs.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);
    });
    ASSERT_EQ(pairs[0].cell_id, 50u);
    ASSERT_EQ(pairs[0].item_id, 1u);
    ASSERT_EQ(pairs[1].cell_id, 50u);
    ASSERT_EQ(pairs[1].item_id, 2u);
    ASSERT_EQ(pairs[2].cell_id, 100u);
    ASSERT_EQ(pairs[2].item_id, 1u);
    PASS();
}

void test_parallel_merge_join_correctness() {
    TEST("merge-join produces correct offsets");
    // Simulate: sorted_cells has cells 10,20,30,40,50
    // sorted_pairs has items for cells 10,30,50
    std::vector<uint64_t> sorted_cells = {10, 20, 30, 40, 50};
    std::vector<CellItemPair> sorted_pairs = {
        {10, 1}, {10, 2},
        {30, 3},
        {50, 4}, {50, 5}, {50, 6}
    };

    // Simulate merge-join
    const uint32_t NO_DATA = 0xFFFFFFFF;
    std::vector<uint32_t> offsets(sorted_cells.size(), NO_DATA);
    uint32_t current = 0;
    size_t pi = 0;
    for (uint32_t si = 0; si < sorted_cells.size() && pi < sorted_pairs.size(); si++) {
        if (sorted_cells[si] < sorted_pairs[pi].cell_id) continue;
        while (pi < sorted_pairs.size() && sorted_pairs[pi].cell_id < sorted_cells[si]) pi++;
        if (pi >= sorted_pairs.size() || sorted_pairs[pi].cell_id != sorted_cells[si]) continue;
        offsets[si] = current;
        size_t start = pi;
        while (pi < sorted_pairs.size() && sorted_pairs[pi].cell_id == sorted_cells[si]) pi++;
        current += sizeof(uint16_t) + (pi - start) * sizeof(uint32_t);
    }

    // Cell 10 (index 0) should have offset 0
    ASSERT_EQ(offsets[0], 0u);
    // Cell 20 (index 1) has no items
    ASSERT_EQ(offsets[1], NO_DATA);
    // Cell 30 (index 2) should have offset after cell 10's data
    uint32_t expected_30 = sizeof(uint16_t) + 2 * sizeof(uint32_t); // count(2) + 2 items
    ASSERT_EQ(offsets[2], expected_30);
    // Cell 40 (index 3) has no items
    ASSERT_EQ(offsets[3], NO_DATA);
    // Cell 50 (index 4) should have offset after cells 10+30
    uint32_t expected_50 = expected_30 + sizeof(uint16_t) + 1 * sizeof(uint32_t);
    ASSERT_EQ(offsets[4], expected_50);
    PASS();
}

void test_ring_area_computation() {
    TEST("ring area computation (shoelace formula)");
    // Unit square: area should be 1.0
    std::vector<std::pair<double,double>> square = {
        {0, 0}, {1, 0}, {1, 1}, {0, 1}, {0, 0}
    };
    double area = 0;
    for (size_t i = 0, n = square.size(); i < n; i++) {
        size_t j = (i + 1) % n;
        area += square[i].first * square[j].second;
        area -= square[j].first * square[i].second;
    }
    area = std::abs(area) / 2.0;
    ASSERT_TRUE(std::abs(area - 1.0) < 1e-10);
    PASS();
}

// --- Main ---

int main() {
    std::cerr << "Running ring assembly unit tests..." << std::endl;

    test_coord_key_basic();
    test_coord_key_split_nodes();
    test_coord_key_negative_coords();
    test_self_intersection_simple_square();
    test_self_intersection_bowtie();
    test_duplicate_coords_detection();
    test_no_duplicate_coords_simple_ring();
    test_border_line_name_pattern();
    test_string_separator_bug();
    test_cell_item_pair_sorting();
    test_parallel_merge_join_correctness();
    test_ring_area_computation();

    std::cerr << std::endl;
    std::cerr << tests_passed << "/" << tests_run << " tests passed." << std::endl;

    return (tests_passed == tests_run) ? 0 : 1;
}

-    for (size_t i = 0, n = square.size(); i < n; i++) {

-        area += square[i].first * square[j].second;

-        area -= square[j].first * square[i].second;

-    ASSERT_TRUE(std::abs(area - 1.0) < 1e-10);

+    ASSERT_TRUE(std::abs(area - 1.0f) < 1e-6f);


Now build and run the test:


Bash

cd /root/traccar-geocoder/builder && g++ -O2 -std=c++17 -o build/test_ring_assembly test/test_ring_assembly.cpp 2>&1 && build/test_ring_assembly 2>&1

Build and run unit tests


Running ring assembly unit tests...
  TEST: coord_key basic... PASS
  TEST: coord_key handles split/replaced nodes at same location... PASS
  TEST: coord_key with negative coordinates... PASS
  TEST: self-intersection: simple square has no crossing... PASS
  TEST: self-intersection: bowtie (figure-8) detected... PASS
  TEST: duplicate coordinate detection in ring... PASS
  TEST: no duplicate coords in simple ring... PASS
  TEST: border-line relation name detection... PASS
  TEST: string null separator (the \0 bug)... PASS
  TEST: CellItemPair sorting by cell_id then item_id... PASS
  TEST: merge-join produces correct offsets... PASS
  TEST: ring area computation (shoelace formula)... PASS

12/12 tests passed.


All 12 tests pass. Let me also verify the main build still compiles:


cd /root/traccar-geocoder/builder/build && make -j$(nproc) 2>&1

Verify main build


[100%] Built target build-index


Updated task #1 status


Updated task #2 status


Now let me add comprehensive unit tests. I'll rewrite the test file to include the existing tests plus many new ones:


     1→// Unit tests for ring assembly algorithms.
     2→// Tests the key edge cases that caused bugs during development:
     3→// - Coordinate-based endpoint matching (split/replaced nodes)
     4→// - Backtracking at branch points
     5→// - Closed-way admin polygon detection
     6→// - Inner-role way retry
     7→// - Self-intersection detection
     8→// - Border-line relation filtering
     9→// - Duplicate coordinate detection (hole merging)
    10→//
    11→// Build: g++ -O2 -std=c++17 -o test_ring_assembly test_ring_assembly.cpp
    12→// Run:   ./test_ring_assembly
    13→
    14→#include <algorithm>
    15→#include <cassert>
    16→#include <cmath>
    17→#include <cstdint>
    18→#include <cstring>
    19→#include <iostream>
    20→#include <string>
    21→#include <unordered_map>
    22→#include <vector>
    23→
    24→#include "../src/types.h"
    25→#include "../src/string_pool.h"
    26→#include "../src/geometry.h"
    27→
    28→// --- Test helpers ---
    29→
    30→static int tests_run = 0;
    31→static int tests_passed = 0;
    32→
    33→#define TEST(name) do { \
    34→    tests_run++; \
    35→    std::cerr << "  TEST: " << name << "... "; \
    36→} while(0)
    37→
    38→#define PASS() do { \
    39→    tests_passed++; \
    40→    std::cerr << "PASS" << std::endl; \
    41→} while(0)
    42→
    43→#define FAIL(msg) do { \
    44→    std::cerr << "FAIL: " << msg << std::endl; \
    45→} while(0)
    46→
    47→template<typename T> std::string to_str(const T& v) { return std::to_string(v); }
    48→inline std::string to_str(const std::string& v) { return "\"" + v + "\""; }
    49→inline std::string to_str(const char* v) { return std::string("\"") + v + "\""; }
    50→
    51→#define ASSERT_EQ(a, b) do { \
    52→    if ((a) != (b)) { FAIL(#a " != " #b " (" + to_str(a) + " vs " + to_str(b) + ")"); return; } \
    53→} while(0)
    54→
    55→#define ASSERT_TRUE(cond) do { \
    56→    if (!(cond)) { FAIL(#cond " is false"); return; } \
    57→} while(0)
    58→
    59→// --- Tests ---
    60→
    61→void test_coord_key_basic() {
    62→    TEST("coord_key basic");
    63→    // Same coordinates should produce same key
    64→    ASSERT_EQ(coord_key(51.5, -0.1), coord_key(51.5, -0.1));
    65→    // Different coordinates should produce different keys
    66→    ASSERT_TRUE(coord_key(51.5, -0.1) != coord_key(51.5, -0.2));
    67→    ASSERT_TRUE(coord_key(51.5, -0.1) != coord_key(51.6, -0.1));
    68→    PASS();
    69→}
    70→
    71→void test_coord_key_split_nodes() {
    72→    TEST("coord_key handles split/replaced nodes at same location");
    73→    // Two different OSM nodes at the same coordinates should have the same key
    74→    // (even though they have different node IDs)
    75→    double lat = 49.5576001, lng = 8.8074189;
    76→    int64_t k1 = coord_key(lat, lng);
    77→    int64_t k2 = coord_key(lat, lng);
    78→    ASSERT_EQ(k1, k2);
    79→    PASS();
    80→}
    81→
    82→void test_coord_key_negative_coords() {
    83→    TEST("coord_key with negative coordinates");
    84→    // Southern/western hemisphere
    85→    int64_t k1 = coord_key(-33.8688, 151.2093); // Sydney
    86→    int64_t k2 = coord_key(-33.8688, 151.2093);
    87→    ASSERT_EQ(k1, k2);
    88→    // Different cities
    89→    ASSERT_TRUE(coord_key(-33.8688, 151.2093) != coord_key(-22.9068, -43.1729));
    90→    PASS();
    91→}
    92→
    93→void test_self_intersection_simple_square() {
    94→    TEST("self-intersection: simple square has no crossing");
    95→    std::vector<std::pair<double,double>> square = {
    96→        {0, 0}, {0, 1}, {1, 1}, {1, 0}, {0, 0}
    97→    };
    98→    ASSERT_TRUE(!ring_has_self_intersection(square));
    99→    PASS();
   100→}
   101→
   102→void test_self_intersection_bowtie() {
   103→    TEST("self-intersection: bowtie (figure-8) detected");
   104→    std::vector<std::pair<double,double>> bowtie = {
   105→        {0, 0}, {1, 1}, {1, 0}, {0, 1}, {0, 0}
   106→    };
   107→    ASSERT_TRUE(ring_has_self_intersection(bowtie));
   108→    PASS();
   109→}
   110→
   111→void test_duplicate_coords_detection() {
   112→    TEST("duplicate coordinate detection in ring");
   113→    std::vector<std::pair<double,double>> ring = {
   114→        {0, 0}, {1, 0}, {1, 1}, {0.5, 0.5}, {0, 1}, {0.5, 0.5}, {1, 1}, {0, 0}
   115→    };
   116→    ASSERT_TRUE(ring_has_duplicate_coords(ring));
   117→    PASS();
   118→}
   119→
   120→void test_no_duplicate_coords_simple_ring() {
   121→    TEST("no duplicate coords in simple ring");
   122→    std::vector<std::pair<double,double>> ring = {
   123→        {0, 0}, {1, 0}, {1, 1}, {0, 1}, {0, 0}
   124→    };
   125→    ASSERT_TRUE(!ring_has_duplicate_coords(ring));
   126→    PASS();
   127→}
   128→
   129→void test_border_line_name_pattern() {
   130→    TEST("border-line relation name detection");
   131→    // Level 2 relations with " - " or " — " in name are border lines, not countries
   132→    auto is_border_line = [](const std::string& name) {
   133→        return name.find(" - ") != std::string::npos ||
   134→               name.find(" \xe2\x80\x94 ") != std::string::npos; // em dash
   135→    };
   136→    ASSERT_TRUE(is_border_line("Deutschland - Österreich"));
   137→    ASSERT_TRUE(is_border_line("France - Deutschland"));
   138→    ASSERT_TRUE(is_border_line("Deutschland \xe2\x80\x94 Schweiz / Suisse / Svizerra"));
   139→    ASSERT_TRUE(!is_border_line("Deutschland"));
   140→    ASSERT_TRUE(!is_border_line("Akrotiri and Dhekelia")); // not a border line
   141→    ASSERT_TRUE(!is_border_line("Österreich"));
   142→    PASS();
   143→}
   144→
   145→void test_string_separator_bug() {
   146→    TEST("string null separator (the \\0 bug)");
   147→    // The original bug: std::string("x") + "\0" doesn't embed a null
   148→    std::string bad = std::string("42") + "\0" + std::string("Main St");
   149→    // This produces "42Main St" — the \0 is an empty string in the concatenation
   150→    // The fix uses std::pair<std::string, std::string> instead
   151→
   152→    // Verify the bug exists
   153→    ASSERT_EQ(bad, std::string("42Main St")); // NOT "42\0Main St"
   154→    ASSERT_EQ(bad.size(), 9u); // "42Main St" = 9 chars (no null embedded)
   155→
   156→    // Verify the fix: pair stores them separately
   157→    std::pair<std::string, std::string> fixed = {"42", "Main St"};
   158→    ASSERT_EQ(fixed.first, "42");
   159→    ASSERT_EQ(fixed.second, "Main St");
   160→    PASS();
   161→}
   162→
   163→void test_cell_item_pair_sorting() {
   164→    TEST("CellItemPair sorting by cell_id then item_id");
   165→    std::vector<CellItemPair> pairs = {
   166→        {100, 3}, {50, 1}, {100, 1}, {50, 2}, {100, 2}
   167→    };
   168→    std::sort(pairs.begin(), pairs.end(), [](const CellItemPair& a, const CellItemPair& b) {
   169→        return a.cell_id < b.cell_id || (a.cell_id == b.cell_id && a.item_id < b.item_id);
   170→    });
   171→    ASSERT_EQ(pairs[0].cell_id, 50u);
   172→    ASSERT_EQ(pairs[0].item_id, 1u);
   173→    ASSERT_EQ(pairs[1].cell_id, 50u);
   174→    ASSERT_EQ(pairs[1].item_id, 2u);
   175→    ASSERT_EQ(pairs[2].cell_id, 100u);
   176→    ASSERT_EQ(pairs[2].item_id, 1u);
   177→    PASS();
   178→}
   179→
   180→void test_parallel_merge_join_correctness() {
   181→    TEST("merge-join produces correct offsets");
   182→    // Simulate: sorted_cells has cells 10,20,30,40,50
   183→    // sorted_pairs has items for cells 10,30,50
   184→    std::vector<uint64_t> sorted_cells = {10, 20, 30, 40, 50};
   185→    std::vector<CellItemPair> sorted_pairs = {
   186→        {10, 1}, {10, 2},
   187→        {30, 3},
   188→        {50, 4}, {50, 5}, {50, 6}
   189→    };
   190→
   191→    // Simulate merge-join
   192→    const uint32_t NO_DATA = 0xFFFFFFFF;
   193→    std::vector<uint32_t> offsets(sorted_cells.size(), NO_DATA);
   194→    uint32_t current = 0;
   195→    size_t pi = 0;
   196→    for (uint32_t si = 0; si < sorted_cells.size() && pi < sorted_pairs.size(); si++) {
   197→        if (sorted_cells[si] < sorted_pairs[pi].cell_id) continue;
   198→        while (pi < sorted_pairs.size() && sorted_pairs[pi].cell_id < sorted_cells[si]) pi++;
   199→        if (pi >= sorted_pairs.size() || sorted_pairs[pi].cell_id != sorted_cells[si]) continue;
   200→        offsets[si] = current;
   201→        size_t start = pi;
   202→        while (pi < sorted_pairs.size() && sorted_pairs[pi].cell_id == sorted_cells[si]) pi++;
   203→        current += sizeof(uint16_t) + (pi - start) * sizeof(uint32_t);
   204→    }
   205→
   206→    // Cell 10 (index 0) should have offset 0
   207→    ASSERT_EQ(offsets[0], 0u);
   208→    // Cell 20 (index 1) has no items
   209→    ASSERT_EQ(offsets[1], NO_DATA);
   210→    // Cell 30 (index 2) should have offset after cell 10's data
   211→    uint32_t expected_30 = sizeof(uint16_t) + 2 * sizeof(uint32_t); // count(2) + 2 items
   212→    ASSERT_EQ(offsets[2], expected_30);
   213→    // Cell 40 (index 3) has no items
   214→    ASSERT_EQ(offsets[3], NO_DATA);
   215→    // Cell 50 (index 4) should have offset after cells 10+30
   216→    uint32_t expected_50 = expected_30 + sizeof(uint16_t) + 1 * sizeof(uint32_t);
   217→    ASSERT_EQ(offsets[4], expected_50);
   218→    PASS();
   219→}
   220→
   221→void test_ring_area_computation() {
   222→    TEST("ring area computation (shoelace formula)");
   223→    std::vector<std::pair<double,double>> square = {
   224→        {0, 0}, {1, 0}, {1, 1}, {0, 1}, {0, 0}
   225→    };
   226→    float area = polygon_area(square);
   227→    ASSERT_TRUE(std::abs(area - 1.0f) < 1e-6f);
   228→    PASS();
   229→}
   230→
   231→// --- Main ---
   232→
   233→int main() {
   234→    std::cerr << "Running ring assembly unit tests..." << std::endl;
   235→
   236→    test_coord_key_basic();
   237→    test_coord_key_split_nodes();
   238→    test_coord_key_negative_coords();
   239→    test_self_intersection_simple_square();
   240→    test_self_intersection_bowtie();
   241→    test_duplicate_coords_detection();
   242→    test_no_duplicate_coords_simple_ring();
   243→    test_border_line_name_pattern();
   244→    test_string_separator_bug();
   245→    test_cell_item_pair_sorting();
   246→    test_parallel_merge_join_correctness();
   247→    test_ring_area_computation();
   248→
   249→    std::cerr << std::endl;
   250→    std::cerr << tests_passed << "/" << tests_run << " tests passed." << std::endl;
   251→
   252→    return (tests_passed == tests_run) ? 0 : 1;
   253→}
   254→

// Unit tests for ring assembly algorithms.
// Tests the key edge cases that caused bugs during development:
// - Coordinate-based endpoint matching (split/replaced nodes)
// - Backtracking at branch points
// - Closed-way admin polygon detection
// - Inner-role way retry
// - Self-intersection detection
// - Border-line relation filtering
// - Duplicate coordinate detection (hole merging)
//
// Build: g++ -O2 -std=c++17 -o test_ring_assembly test_ring_assembly.cpp
// Run:   ./test_ring_assembly

#include <algorithm>
#include <cassert>
#include <cmath>
#include <cstdint>
#include <cstring>
#include <iostream>
#include <string>
#include <unordered_map>
#include <vector>

#include "../src/types.h"
#include "../src/string_pool.h"
#include "../src/geometry.h"

// --- Test helpers ---

static int tests_run = 0;
static int tests_passed = 0;

#define TEST(name) do { \
    tests_run++; \
    std::cerr << "  TEST: " << name << "... "; \
} while(0)

#define PASS() do { \
    tests_passed++; \
    std::cerr << "PASS" << std::endl; \
} while(0)

#define FAIL(msg) do { \
    std::cerr << "FAIL: " << msg << std::endl; \
} while(0)

template<typename T> std::string to_str(const T& v) { return std::to_string(v); }
inline std::string to_str(const std::string& v) { return "\"" + v + "\""; }
inline std::string to_str(const char* v) { return std::string("\"") + v + "\""; }

#define ASSERT_EQ(a, b) do { \
    if ((a) != (b)) { FAIL(#a " != " #b " (" + to_str(a) + " vs " + to_str(b) + ")"); return; } \
} while(0)

#define ASSERT_TRUE(cond) do { \
    if (!(cond)) { FAIL(#cond " is false"); return; } \
} while(0)

// --- Tests ---

void test_coord_key_basic() {
    TEST("coord_key basic");
    // Same coordinates should produce same key
    ASSERT_EQ(coord_key(51.5, -0.1), coord_key(51.5, -0.1));
    // Different coordinates should produce different keys
    ASSERT_TRUE(coord_key(51.5, -0.1) != coord_key(51.5, -0.2));
    ASSERT_TRUE(coord_key(51.5, -0.1) != coord_key(51.6, -0.1));
    PASS();
}

void test_coord_key_split_nodes() {
    TEST("coord_key handles split/replaced nodes at same location");
    // Two different OSM nodes at the same coordinates should have the same key
    // (even though they have different node IDs)
    double lat = 49.5576001, lng = 8.8074189;
    int64_t k1 = coord_key(lat, lng);
    int64_t k2 = coord_key(lat, lng);
    ASSERT_EQ(k1, k2);
    PASS();
}

void test_coord_key_negative_coords() {
    TEST("coord_key with negative coordinates");
    // Southern/western hemisphere
    int64_t k1 = coord_key(-33.8688, 151.2093); // Sydney
    int64_t k2 = coord_key(-33.8688, 151.2093);
    ASSERT_EQ(k1, k2);
    // Different cities
    ASSERT_TRUE(coord_key(-33.8688, 151.2093) != coord_key(-22.9068, -43.1729));
    PASS();
}

void test_self_intersection_simple_square() {
    TEST("self-intersection: simple square has no crossing");
    std::vector<std::pair<double,double>> square = {
        {0, 0}, {0, 1}, {1, 1}, {1, 0}, {0, 0}
    };
    ASSERT_TRUE(!ring_has_self_intersection(square));
    PASS();
}

void test_self_intersection_bowtie() {
    TEST("self-intersection: bowtie (figure-8) detected");
    std::vector<std::pair<double,double>> bowtie = {
        {0, 0}, {1, 1}, {1, 0}, {0, 1}, {0, 0}
    };
    ASSERT_TRUE(ring_has_self_intersection(bowtie));
    PASS();
}

void test_duplicate_coords_detection() {
    TEST("duplicate coordinate detection in ring");
    std::vector<std::pair<double,double>> ring = {
        {0, 0}, {1, 0}, {1, 1}, {0.5, 0.5}, {0, 1}, {0.5, 0.5}, {1, 1}, {0, 0}
    };
    ASSERT_TRUE(ring_has_duplicate_coords(ring));
    PASS();
}

void test_no_duplicate_coords_simple_ring() {
    TEST("no duplicate coords in simple ring");
    std::vector<std::pair<double,double>> ring = {
        {0, 0}, {1, 0}, {1, 1}, {0, 1}, {0, 0}
    };
    ASSERT_TRUE(!ring_has_duplicate_coords(ring));
    PASS();
}

void test_border_line_name_pattern() {
    TEST("border-line relation name detection");
    // Level 2 relations with " - " or " — " in name are border lines, not countries
    auto is_border_line = [](const std::string& name) {
        return name.find(" - ") != std::string::npos ||
               name.find(" \xe2\x80\x94 ") != std::string::npos; // em dash
    };
    ASSERT_TRUE(is_border_line("Deutschland - Österreich"));
    ASSERT_TRUE(is_border_line("France - Deutschland"));
    ASSERT_TRUE(is_border_line("Deutschland \xe2\x80\x94 Schweiz / Suisse / Svizerra"));
    ASSERT_TRUE(!is_border_line("Deutschland"));
    ASSERT_TRUE(!is_border_line("Akrotiri and Dhekelia")); // not a border line
    ASSERT_TRUE(!is_border_line("Österreich"));
    PASS();
}

void test_string_separator_bug() {
    TEST("string null separator (the \\0 bug)");
    // The original bug: std::string("x") + "\0" doesn't embed a null
    std::string bad = std::string("42") + "\0" + std::string("Main St");
    // This produces "42Main St" — the \0 is an empty string in the concatenation
    // The fix uses std::pair<std::string, std::string> instead

    // Verify the bug exists
    ASSERT_EQ(bad, std::string("42Main St")); // NOT "42\0Main St"
    ASSERT_EQ(bad.size(), 9u); // "42Main St" = 9 chars (no null embedded)

    // Verify the fix: pair stores them separately
    std::pair<std::string, std::string> fixed = {"42", "Main St"};
    ASSERT_EQ(fixed.first, "42");
    ASSERT_EQ(fixed.second, "Main St");
    PASS();
}

void test_cell_item_pair_sorting() {
    TEST("CellItemPair sorting by cell_id then item_id");
    std::vector<CellItemPair> pairs = {
        {100, 3}, {50, 1}, {100, 1}, {50, 2}, {100, 2}
    };
    std::sort(pairs.begin(), pairs.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);
    });
    ASSERT_EQ(pairs[0].cell_id, 50u);
    ASSERT_EQ(pairs[0].item_id, 1u);
    ASSERT_EQ(pairs[1].cell_id, 50u);
    ASSERT_EQ(pairs[1].item_id, 2u);
    ASSERT_EQ(pairs[2].cell_id, 100u);
    ASSERT_EQ(pairs[2].item_id, 1u);
    PASS();
}

void test_parallel_merge_join_correctness() {
    TEST("merge-join produces correct offsets");
    // Simulate: sorted_cells has cells 10,20,30,40,50
    // sorted_pairs has items for cells 10,30,50
    std::vector<uint64_t> sorted_cells = {10, 20, 30, 40, 50};
    std::vector<CellItemPair> sorted_pairs = {
        {10, 1}, {10, 2},
        {30, 3},
        {50, 4}, {50, 5}, {50, 6}
    };

    // Simulate merge-join
    const uint32_t NO_DATA = 0xFFFFFFFF;
    std::vector<uint32_t> offsets(sorted_cells.size(), NO_DATA);
    uint32_t current = 0;
    size_t pi = 0;
    for (uint32_t si = 0; si < sorted_cells.size() && pi < sorted_pairs.size(); si++) {
        if (sorted_cells[si] < sorted_pairs[pi].cell_id) continue;
        while (pi < sorted_pairs.size() && sorted_pairs[pi].cell_id < sorted_cells[si]) pi++;
        if (pi >= sorted_pairs.size() || sorted_pairs[pi].cell_id != sorted_cells[si]) continue;
        offsets[si] = current;
        size_t start = pi;
        while (pi < sorted_pairs.size() && sorted_pairs[pi].cell_id == sorted_cells[si]) pi++;
        current += sizeof(uint16_t) + (pi - start) * sizeof(uint32_t);
    }

    // Cell 10 (index 0) should have offset 0
    ASSERT_EQ(offsets[0], 0u);
    // Cell 20 (index 1) has no items
    ASSERT_EQ(offsets[1], NO_DATA);
    // Cell 30 (index 2) should have offset after cell 10's data
    uint32_t expected_30 = sizeof(uint16_t) + 2 * sizeof(uint32_t); // count(2) + 2 items
    ASSERT_EQ(offsets[2], expected_30);
    // Cell 40 (index 3) has no items
    ASSERT_EQ(offsets[3], NO_DATA);
    // Cell 50 (index 4) should have offset after cells 10+30
    uint32_t expected_50 = expected_30 + sizeof(uint16_t) + 1 * sizeof(uint32_t);
    ASSERT_EQ(offsets[4], expected_50);
    PASS();
}

void test_ring_area_computation() {
    TEST("ring area computation (shoelace formula)");
    std::vector<std::pair<double,double>> square = {
        {0, 0}, {1, 0}, {1, 1}, {0, 1}, {0, 0}
    };
    float area = polygon_area(square);
    ASSERT_TRUE(std::abs(area - 1.0f) < 1e-6f);
    PASS();
}

// --- Main ---

int main() {
    std::cerr << "Running ring assembly unit tests..." << std::endl;

    test_coord_key_basic();
    test_coord_key_split_nodes();
    test_coord_key_negative_coords();
    test_self_intersection_simple_square();
    test_self_intersection_bowtie();
    test_duplicate_coords_detection();
    test_no_duplicate_coords_simple_ring();
    test_border_line_name_pattern();
    test_string_separator_bug();
    test_cell_item_pair_sorting();
    test_parallel_merge_join_correctness();
    test_ring_area_computation();

    std::cerr << std::endl;
    std::cerr << tests_passed << "/" << tests_run << " tests passed." << std::endl;

    return (tests_passed == tests_run) ? 0 : 1;
}


Now I'll add the comprehensive new tests after the existing ones:


Edit

// --- Main ---

int main() {
    std::cerr << "Running ring assembly unit tests..." << std::endl;

    test_coord_key_basic();
    test_coord_key_split_nodes();
    test_coord_key_negative_coords();
    test_self_intersection_simple_square();
    test_self_intersection_bowtie();
    test_duplicate_coords_detection();
    test_no_duplicate_coords_simple_ring();
    test_border_line_name_pattern();
    test_string_separator_bug();
    test_cell_item_pair_sorting();
    test_parallel_merge_join_correctness();
    test_ring_area_computation();

    std::cerr << std::endl;
    std::cerr << tests_passed << "/" << tests_run << " tests passed." << std::endl;

    return (tests_passed == tests_run) ? 0 : 1;
}

// --- New tests: coord_key edge cases ---

void test_coord_key_equator_prime_meridian() {
    TEST("coord_key at equator/prime meridian (0,0)");
    int64_t k = coord_key(0.0, 0.0);
    ASSERT_TRUE(k != coord_key(0.0001, 0.0));
    ASSERT_TRUE(k != coord_key(0.0, 0.0001));
    PASS();
}

void test_coord_key_max_values() {
    TEST("coord_key at extreme coordinates");
    // North pole area
    int64_t k1 = coord_key(89.9999, 179.9999);
    int64_t k2 = coord_key(-89.9999, -179.9999);
    ASSERT_TRUE(k1 != k2);
    // Very close but distinct
    ASSERT_TRUE(coord_key(51.5000001, -0.1) != coord_key(51.5000002, -0.1));
    PASS();
}

void test_coord_key_precision_boundary() {
    TEST("coord_key precision at 1e-7 degree boundary");
    // 1e-7 degrees is ~1cm at equator — our quantization boundary
    // These should be equal (within quantization)
    ASSERT_EQ(coord_key(51.50000004, -0.1), coord_key(51.50000004, -0.1));
    // These should differ (different quantized values)
    ASSERT_TRUE(coord_key(51.5000000, -0.1) != coord_key(51.5000002, -0.1));
    PASS();
}

// --- New tests: segments_intersect ---

void test_segments_parallel() {
    TEST("segments_intersect: parallel segments don't intersect");
    ASSERT_TRUE(!segments_intersect(0,0, 1,0,  0,1, 1,1));
    PASS();
}

void test_segments_collinear() {
    TEST("segments_intersect: collinear overlapping segments");
    // Collinear segments: our cross-product test returns false (no proper crossing)
    ASSERT_TRUE(!segments_intersect(0,0, 2,0,  1,0, 3,0));
    PASS();
}

void test_segments_t_junction() {
    TEST("segments_intersect: T-junction (endpoint on segment)");
    // Endpoint touching — not a proper crossing
    ASSERT_TRUE(!segments_intersect(0,0, 2,0,  1,0, 1,1));
    PASS();
}

void test_segments_clear_cross() {
    TEST("segments_intersect: clear X crossing");
    ASSERT_TRUE(segments_intersect(0,0, 1,1,  0,1, 1,0));
    PASS();
}

void test_segments_no_cross() {
    TEST("segments_intersect: clearly separate segments");
    ASSERT_TRUE(!segments_intersect(0,0, 1,0,  2,2, 3,2));
    PASS();
}

// --- New tests: ring_has_self_intersection ---

void test_self_intersection_triangle() {
    TEST("self-intersection: triangle has no crossing");
    std::vector<std::pair<double,double>> tri = {
        {0, 0}, {1, 0}, {0.5, 1}, {0, 0}
    };
    ASSERT_TRUE(!ring_has_self_intersection(tri));
    PASS();
}

void test_self_intersection_too_few_points() {
    TEST("self-intersection: fewer than 4 points returns false");
    std::vector<std::pair<double,double>> tiny = {{0,0}, {1,0}, {0,0}};
    ASSERT_TRUE(!ring_has_self_intersection(tiny));
    PASS();
}

void test_self_intersection_large_ring_clean() {
    TEST("self-intersection: large clean ring (sweep-line path)");
    // Create a 100-point ring (circle) — no self-intersection
    std::vector<std::pair<double,double>> ring;
    for (int i = 0; i < 100; i++) {
        double angle = 2.0 * M_PI * i / 100.0;
        ring.push_back({std::cos(angle), std::sin(angle)});
    }
    ring.push_back(ring.front());
    ASSERT_TRUE(!ring_has_self_intersection(ring));
    PASS();
}

void test_self_intersection_large_ring_crossed() {
    TEST("self-intersection: large ring with crossing");
    // Circle with two swapped points to create crossing
    std::vector<std::pair<double,double>> ring;
    for (int i = 0; i < 100; i++) {
        double angle = 2.0 * M_PI * i / 100.0;
        ring.push_back({std::cos(angle), std::sin(angle)});
    }
    ring.push_back(ring.front());
    // Swap two non-adjacent points to create crossing
    std::swap(ring[20], ring[80]);
    ASSERT_TRUE(ring_has_self_intersection(ring));
    PASS();
}

// --- New tests: polygon_area ---

void test_polygon_area_triangle() {
    TEST("polygon_area: right triangle");
    std::vector<std::pair<double,double>> tri = {
        {0, 0}, {2, 0}, {0, 3}, {0, 0}
    };
    float area = polygon_area(tri);
    ASSERT_TRUE(std::abs(area - 3.0f) < 1e-6f);
    PASS();
}

void test_polygon_area_degenerate() {
    TEST("polygon_area: degenerate (line) has zero area");
    std::vector<std::pair<double,double>> line = {
        {0, 0}, {1, 0}, {2, 0}, {0, 0}
    };
    float area = polygon_area(line);
    ASSERT_TRUE(std::abs(area) < 1e-6f);
    PASS();
}

void test_polygon_area_clockwise() {
    TEST("polygon_area: clockwise and counterclockwise same result");
    std::vector<std::pair<double,double>> ccw = {
        {0, 0}, {1, 0}, {1, 1}, {0, 1}, {0, 0}
    };
    std::vector<std::pair<double,double>> cw = {
        {0, 0}, {0, 1}, {1, 1}, {1, 0}, {0, 0}
    };
    ASSERT_TRUE(std::abs(polygon_area(ccw) - polygon_area(cw)) < 1e-6f);
    PASS();
}

// --- New tests: simplify_polygon ---

void test_simplify_already_small() {
    TEST("simplify_polygon: already below threshold returns unchanged");
    std::vector<std::pair<double,double>> pts = {
        {0, 0}, {1, 0}, {1, 1}, {0, 1}, {0, 0}
    };
    auto result = simplify_polygon(pts, 10);
    ASSERT_EQ(result.size(), pts.size());
    PASS();
}

void test_simplify_reduces_points() {
    TEST("simplify_polygon: large polygon gets reduced");
    // Create a 200-point near-circle
    std::vector<std::pair<double,double>> ring;
    for (int i = 0; i < 200; i++) {
        double angle = 2.0 * M_PI * i / 200.0;
        ring.push_back({std::cos(angle), std::sin(angle)});
    }
    ring.push_back(ring.front());
    auto result = simplify_polygon(ring, 50);
    ASSERT_TRUE(result.size() <= 50);
    ASSERT_TRUE(result.size() >= 3);
    PASS();
}

// --- New tests: parse_house_number ---

void test_parse_house_number_basic() {
    TEST("parse_house_number: basic number");
    ASSERT_EQ(parse_house_number("42"), 42u);
    PASS();
}

void test_parse_house_number_alpha_suffix() {
    TEST("parse_house_number: number with letter suffix");
    ASSERT_EQ(parse_house_number("42a"), 42u);
    ASSERT_EQ(parse_house_number("123B"), 123u);
    PASS();
}

void test_parse_house_number_empty() {
    TEST("parse_house_number: empty string");
    ASSERT_EQ(parse_house_number(""), 0u);
    PASS();
}

void test_parse_house_number_null() {
    TEST("parse_house_number: null pointer");
    ASSERT_EQ(parse_house_number(nullptr), 0u);
    PASS();
}

void test_parse_house_number_no_digits() {
    TEST("parse_house_number: no leading digits");
    ASSERT_EQ(parse_house_number("abc"), 0u);
    PASS();
}

void test_parse_house_number_range() {
    TEST("parse_house_number: range string");
    ASSERT_EQ(parse_house_number("10-12"), 10u);
    PASS();
}

// --- New tests: is_included_highway ---

void test_highway_excluded_types() {
    TEST("is_included_highway: excluded types");
    ASSERT_TRUE(!is_included_highway("footway"));
    ASSERT_TRUE(!is_included_highway("path"));
    ASSERT_TRUE(!is_included_highway("track"));
    ASSERT_TRUE(!is_included_highway("steps"));
    ASSERT_TRUE(!is_included_highway("cycleway"));
    ASSERT_TRUE(!is_included_highway("service"));
    ASSERT_TRUE(!is_included_highway("pedestrian"));
    ASSERT_TRUE(!is_included_highway("bridleway"));
    ASSERT_TRUE(!is_included_highway("construction"));
    PASS();
}

void test_highway_included_types() {
    TEST("is_included_highway: included types");
    ASSERT_TRUE(is_included_highway("residential"));
    ASSERT_TRUE(is_included_highway("primary"));
    ASSERT_TRUE(is_included_highway("secondary"));
    ASSERT_TRUE(is_included_highway("tertiary"));
    ASSERT_TRUE(is_included_highway("motorway"));
    ASSERT_TRUE(is_included_highway("trunk"));
    ASSERT_TRUE(is_included_highway("living_street"));
    ASSERT_TRUE(is_included_highway("unclassified"));
    PASS();
}

// --- New tests: StringPool ---

void test_string_pool_basic() {
    TEST("StringPool: basic intern and retrieve");
    StringPool pool;
    uint32_t id1 = pool.intern("hello");
    uint32_t id2 = pool.intern("world");
    ASSERT_TRUE(id1 != id2);
    ASSERT_EQ(std::string(pool.data().data() + id1), std::string("hello"));
    ASSERT_EQ(std::string(pool.data().data() + id2), std::string("world"));
    PASS();
}

void test_string_pool_dedup() {
    TEST("StringPool: duplicate strings return same offset");
    StringPool pool;
    uint32_t id1 = pool.intern("test");
    uint32_t id2 = pool.intern("test");
    ASSERT_EQ(id1, id2);
    PASS();
}

void test_string_pool_empty_string() {
    TEST("StringPool: empty string");
    StringPool pool;
    uint32_t id = pool.intern("");
    ASSERT_EQ(std::string(pool.data().data() + id), std::string(""));
    ASSERT_EQ(pool.data()[id], '\0');
    PASS();
}

void test_string_pool_order() {
    TEST("StringPool: first intern gets offset 0");
    StringPool pool;
    uint32_t id = pool.intern("first");
    ASSERT_EQ(id, 0u);
    // Second string starts after "first\0" = 6 bytes
    uint32_t id2 = pool.intern("second");
    ASSERT_EQ(id2, 6u);
    PASS();
}

// --- New tests: ring_has_duplicate_coords edge cases ---

void test_duplicate_coords_closing_point() {
    TEST("duplicate coords: closing point not counted as duplicate");
    // First and last point are the same — this is normal for a closed ring
    std::vector<std::pair<double,double>> ring = {
        {0, 0}, {1, 0}, {1, 1}, {0, 0}
    };
    ASSERT_TRUE(!ring_has_duplicate_coords(ring));
    PASS();
}

void test_duplicate_coords_two_point() {
    TEST("duplicate coords: two-point ring");
    std::vector<std::pair<double,double>> ring = {{0, 0}, {0, 0}};
    // Only one point checked (skip closing), no duplicate
    ASSERT_TRUE(!ring_has_duplicate_coords(ring));
    PASS();
}

// --- New tests: dp_simplify ---

void test_dp_simplify_straight_line() {
    TEST("dp_simplify: points on a straight line collapse");
    std::vector<std::pair<double,double>> pts = {
        {0, 0}, {0.5, 0}, {1, 0}, {1.5, 0}, {2, 0}
    };
    std::vector<bool> keep(pts.size(), false);
    keep[0] = true;
    keep[pts.size() - 1] = true;
    dp_simplify(pts, 0, pts.size() - 1, 0.01, keep);
    // Only first and last should be kept — interior points are on the line
    int count = 0;
    for (bool k : keep) if (k) count++;
    ASSERT_EQ(count, 2);
    PASS();
}

void test_dp_simplify_preserves_peak() {
    TEST("dp_simplify: preserves peak point");
    std::vector<std::pair<double,double>> pts = {
        {0, 0}, {0.5, 1}, {1, 0}
    };
    std::vector<bool> keep(pts.size(), false);
    keep[0] = true;
    keep[pts.size() - 1] = true;
    dp_simplify(pts, 0, pts.size() - 1, 0.01, keep);
    // The peak at (0.5, 1) should be kept
    ASSERT_TRUE(keep[1]);
    PASS();
}

// --- New tests: binary structs ---

void test_way_header_size() {
    TEST("WayHeader struct size");
    // Ensure struct is packed as expected
    ASSERT_TRUE(sizeof(WayHeader) > 0);
    ASSERT_TRUE(sizeof(WayHeader) <= 16); // should be compact
    PASS();
}

void test_admin_polygon_struct() {
    TEST("AdminPolygon country_code encoding");
    AdminPolygon ap{};
    const char* cc = "US";
    ap.country_code = static_cast<uint16_t>((cc[0] << 8) | cc[1]);
    ASSERT_EQ(ap.country_code >> 8, (uint16_t)'U');
    ASSERT_EQ(ap.country_code & 0xFF, (uint16_t)'S');
    PASS();
}

void test_interior_flag() {
    TEST("INTERIOR_FLAG and ID_MASK are complementary");
    uint32_t id = 12345;
    uint32_t flagged = id | INTERIOR_FLAG;
    ASSERT_EQ(flagged & ID_MASK, id);
    ASSERT_TRUE(flagged & INTERIOR_FLAG);
    ASSERT_TRUE(!(id & INTERIOR_FLAG));
    PASS();
}

// --- Main ---

int main() {
    std::cerr << "Running unit tests..." << std::endl;

    // Original tests
    test_coord_key_basic();
    test_coord_key_split_nodes();
    test_coord_key_negative_coords();
    test_self_intersection_simple_square();
    test_self_intersection_bowtie();
    test_duplicate_coords_detection();
    test_no_duplicate_coords_simple_ring();
    test_border_line_name_pattern();
    test_string_separator_bug();
    test_cell_item_pair_sorting();
    test_parallel_merge_join_correctness();
    test_ring_area_computation();

    // New: coord_key edge cases
    test_coord_key_equator_prime_meridian();
    test_coord_key_max_values();
    test_coord_key_precision_boundary();

    // New: segments_intersect
    test_segments_parallel();
    test_segments_collinear();
    test_segments_t_junction();
    test_segments_clear_cross();
    test_segments_no_cross();

    // New: ring_has_self_intersection
    test_self_intersection_triangle();
    test_self_intersection_too_few_points();
    test_self_intersection_large_ring_clean();
    test_self_intersection_large_ring_crossed();

    // New: polygon_area
    test_polygon_area_triangle();
    test_polygon_area_degenerate();
    test_polygon_area_clockwise();

    // New: simplify_polygon
    test_simplify_already_small();
    test_simplify_reduces_points();

    // New: parse_house_number
    test_parse_house_number_basic();
    test_parse_house_number_alpha_suffix();
    test_parse_house_number_empty();
    test_parse_house_number_null();
    test_parse_house_number_no_digits();
    test_parse_house_number_range();

    // New: is_included_highway
    test_highway_excluded_types();
    test_highway_included_types();

    // New: StringPool
    test_string_pool_basic();
    test_string_pool_dedup();
    test_string_pool_empty_string();
    test_string_pool_order();

    // New: ring_has_duplicate_coords edge cases
    test_duplicate_coords_closing_point();
    test_duplicate_coords_two_point();

    // New: dp_simplify
    test_dp_simplify_straight_line();
    test_dp_simplify_preserves_peak();

    // New: binary structs
    test_way_header_size();
    test_admin_polygon_struct();
    test_interior_flag();

    std::cerr << std::endl;
    std::cerr << tests_passed << "/" << tests_run << " tests passed." << std::endl;

    return (tests_passed == tests_run) ? 0 : 1;
}