diff --git a/core/1623-6_border.gpx b/core/1623-6_border.gpx
new file mode 100644
index 000000000..e64adddef
--- /dev/null
+++ b/core/1623-6_border.gpx
@@ -0,0 +1,147 @@
+
+
+ 1626/3-as-trk
+ just the 1623/6 border
+
+ 1626/3-as-trk
+ 1
+
+
+
+
+1626-22
+1626-21
+1626-20
+1626-19
+1626-18
+1626-17
+1626-16
+1626-15
+1626-14
+1626-13
+1626-12
+1626-11
+1626-10
+1626-9
+1626-8
+1626-7
+1626-6
+1626-5
+1626-4
+1626-3
+1626-2
+1626-1
+1626-323
+1626-322
+1626-321
+1626-320
+1626-319
+1626-318
+1626-317
+1626-316
+1626-315
+1626-314
+1626-313
+1626-312
+1626-311
+1626-310
+1626-309
+1626-308
+1626-307
+1626-306
+1626-305
+1626-304
+1626-303
+1623-77
+1623-78
+1623-79
+1623-80
+1626-299
+1623-81
+1626-298
+1623-82
+1626-297
+1623-83
+1626-296
+1623-84
+1626-295
+1623-85
+1626-294
+1623-86
+1626-293
+1623-87
+1626-292
+1623-88
+1626-291
+1623-89
+1626-290
+1623-90
+1626-289
+1623-91
+1626-288
+1623-92
+1626-287
+1623-93
+1626-286
+1623-94
+1626-285
+1623-95
+1626-284
+1623-96
+1626-283
+1623-97
+1626-282
+1623-98
+1626-281
+1623-99
+1626-280
+1623-100
+1626-279
+1623-101
+1626-278
+1623-102
+1626-277
+1623-103
+1626-276
+1623-104
+1626-275
+1623-105
+1626-274
+1623-106
+1626-273
+1623-107
+1626-272
+1623-108
+1626-271
+1623-109
+1626-270
+1623-110
+1626-269
+1623-111
+1626-268
+1623-112
+1626-267
+1623-113
+1626-266
+1623-114
+1626-265
+1623-115
+1626-264
+1623-116
+1626-263
+1623-117
+1626-262
+1623-118
+1626-261
+1623-119
+1626-260
+1623-120
+1626-259
+1623-121
+1626-258
+1623-122
+1626-257
+1623-123
+1626-256
+
+
diff --git a/core/cleaned_border_output.gpx b/core/cleaned_border_output.gpx
new file mode 100644
index 000000000..6af5be294
--- /dev/null
+++ b/core/cleaned_border_output.gpx
@@ -0,0 +1,7 @@
+
+
+
+ Cleaned Border 1626-1623
+
+
+
\ No newline at end of file
diff --git a/core/position_tests.py b/core/position_tests.py
new file mode 100644
index 000000000..bdd8ca019
--- /dev/null
+++ b/core/position_tests.py
@@ -0,0 +1,80 @@
+import xml.etree.ElementTree as ET
+
+from position_utils import which_area # file-type import, not module type.
+
+def run_limit_tests(which_area_func):
+ # Boundary limits derived from GPX data
+ west_limit = 13.72476763 # 1626-22 [cite: 1]
+ east_limit = 13.86031535 # 1626-256 [cite: 3]
+
+ print("Running Border Logic Tests...")
+
+ # 1. Invalid point to the West
+ res, area = which_area_func(47.69, west_limit - 0.01)
+ print(f"Test West: Expected (False, 'None'), Got ({res}, '{area}')")
+
+ # 2. Invalid point to the East
+ res, area = which_area_func(47.70, east_limit + 0.01)
+ print(f"Test East: Expected (False, 'None'), Got ({res}, '{area}')")
+
+
+def generate_test_data():
+ offset = 0.0005
+
+ # 6 base points extracted from the track to ensure we cover the whole E/W span
+ # Coordinates from source GPX
+
+ base_coords = [
+ (47.69048595, 13.72476763 + 2* offset), # Far West (1626-22)
+ (47.68532296, 13.73512238), # Monotonic section 1 (1626-13)
+ (47.67441976, 13.76998479), # Central dip (1626-308)
+ (47.68909077, 13.79091566), # Rising section (1626-294)
+ (47.70536617, 13.82402883), # Northern peak (1626-278)
+ (47.70002466, 13.86031535 - 2* offset) # Far East (1626-256)
+ ]
+
+ test_points = []
+
+ for lat, lon in base_coords:
+ # Generate North point (Area 1626)
+ test_points.append({'lat': lat + offset, 'lon': lon + offset, 'area': '1626', 'desc': 'North_Test'})
+ # Generate South point (Area 1623)
+ test_points.append({'lat': lat - offset, 'lon': lon - offset, 'area': '1623', 'desc': 'South_Test'})
+
+ return test_points
+
+def export_test_gpx(test_points, filename="test_points.gpx"):
+ gpx = ET.Element("gpx", version="1.1", creator="Python Script",
+ xmlns="http://www.topografix.com/GPX/1/1")
+
+ for pt in test_points:
+ wpt = ET.SubElement(gpx, "wpt", lat=str(pt['lat']), lon=str(pt['lon']))
+ name = ET.SubElement(wpt, "name")
+ name.text = f"{pt['area']}_{pt['desc']}"
+ desc = ET.SubElement(wpt, "desc")
+ desc.text = f"Expected Area: {pt['area']}"
+
+ tree = ET.ElementTree(gpx)
+ tree.write(filename, encoding="utf-8", xml_declaration=True)
+ print(f"Successfully created {filename} with 12 test waypoints.")
+
+def run_12_point_test(which_area_func):
+ points = generate_test_data()
+ export_test_gpx(points)
+
+ passed = 0
+
+ print(f"{'Lat':<12} | {'Lon':<12} | {'Expected':<10} | {'Result'}")
+ print("-" * 55)
+
+ for pt in points:
+ valid, area = which_area_func(pt['lat'], pt['lon'])
+ status = "PASS" if (valid and area == pt['area']) else "FAIL"
+ if status == "PASS": passed += 1
+ print(f"{pt['lat']:<12.6f} | {pt['lon']:<12.6f} | {pt['area']:<10} | {status}")
+
+ print(f"\nSummary: {passed}/12 points passed.")
+
+# Uncomment to run:
+run_limit_tests(which_area)
+run_12_point_test(which_area)
\ No newline at end of file
diff --git a/core/position_utils.py b/core/position_utils.py
new file mode 100644
index 000000000..c0168a630
--- /dev/null
+++ b/core/position_utils.py
@@ -0,0 +1,184 @@
+import bisect
+from pathlib import Path
+import xml.etree.ElementTree as ET
+
+def load_and_clean_gpx(filename):
+ if not Path(filename).exists:
+ print("No file")
+ else:
+ print(f"Loading {filename}")
+ # Parse GPX
+ tree = ET.parse(filename)
+ root = tree.getroot()
+
+ # Extract points (handling namespaces)
+ ns = {'gpx': 'http://www.topografix.com/GPX/1/0'}
+ raw_points = []
+ for pt in root.findall('.//gpx:trkpt', ns):
+ lat = float(pt.get('lat'))
+ lon = float(pt.get('lon'))
+ raw_points.append((lon, lat)) # Using (x, y) order for easier logic
+
+ n_raw = len(raw_points)
+ if not raw_points:
+ return []
+
+ # 1. Remove exactly duplicate consecutive points
+ cleaned_points = [raw_points[0]]
+ for i in range(1, len(raw_points)):
+ if raw_points[i] != raw_points[i-1]:
+ cleaned_points.append(raw_points[i])
+
+ n_clean =len(cleaned_points)
+ print(f"read {n_raw} points and returned {n_clean} cleaned points")
+ return cleaned_points
+
+
+
+def save_cleaned_gpx(points, output_filename="cleaned_border.gpx"):
+ """
+ Saves a list of (lon, lat) tuples as a GPX track.
+ """
+ # Create the root element with necessary namespaces
+ root = ET.Element("gpx",
+ version="1.1",
+ creator="CaveAreaProcessor",
+ xmlns="http://www.topografix.com/GPX/1/1")
+
+ # Create the track structure
+ trk = ET.SubElement(root, "trk")
+ name = ET.SubElement(trk, "name")
+ name.text = "Cleaned Border 1626-1623"
+
+ trkseg = ET.SubElement(trk, "trkseg")
+
+ # Add each point as a track point
+ for lon, lat in points:
+ # Note: GPX uses lat/lon attributes, we provide them as strings
+ ET.SubElement(trkseg, "trkpt", lat=str(lat), lon=str(lon))
+
+ # Write to file
+ tree = ET.ElementTree(root)
+ # Use indenting for readability if available (Python 3.9+)
+ if hasattr(ET, "indent"):
+ ET.indent(tree, space="\t", level=0)
+
+ tree.write(output_filename, encoding="windows-1252", xml_declaration=True)
+ print(f"Cleaned track exported to {output_filename}")
+
+
+def split_into_monotonic_segments(points):
+ """
+ Splits the track into segments that are monotonic in Longitude (E/W).
+ This allows us to use binary search on each segment.
+ """
+ if not points: return []
+
+ segments = []
+ current_segment = [points[0]]
+
+ for i in range(1, len(points)):
+ p_prev = points[i-1]
+ p_curr = points[i]
+
+ # Check if we should start a new segment based on direction change in Lon
+ if len(current_segment) > 1:
+ prev_dir = current_segment[-1][0] - current_segment[-2][0]
+ curr_dir = p_curr[0] - p_prev[0]
+
+ # If direction changed (and wasn't zero before), split
+ if (prev_dir > 0 and curr_dir < 0) or (prev_dir < 0 and curr_dir > 0):
+ segments.append(current_segment)
+ current_segment = [p_prev]
+
+ current_segment.append(p_curr)
+
+ segments.append(current_segment)
+ print(len(segments))
+ return segments
+
+
+
+
+PREPARED_SEGMENTS = []
+MIN_LON = 0
+MAX_LON = 0
+
+def generate_boundary_segments():
+ # Example Workflow:
+ points = load_and_clean_gpx('1623-6_border.gpx')
+ # save_cleaned_gpx(points, "cleaned_border_output.gpx") # done once, not needed
+ mono_segments = split_into_monotonic_segments(points)
+
+ # Pre-calculate global bounds and segment metadata
+ ALL_LONS = [p[0] for p in points]
+ MIN_LON, MAX_LON = min(ALL_LONS), max(ALL_LONS)
+
+ # Prepare segments for binary search
+ # We store each segment as (sorted_lons, corresponding_lats)
+ for seg in mono_segments:
+ lons = [p[0] for p in seg]
+ lats = [p[1] for p in seg]
+
+ # Ensure lons are strictly increasing for bisect
+ is_increasing = lons[-1] > lons[0]
+ if not is_increasing:
+ lons.reverse()
+ lats.reverse()
+
+ PREPARED_SEGMENTS.append({
+ 'lons': lons,
+ 'lats': lats,
+ 'min_lon': min(lons),
+ 'max_lon': max(lons)
+ })
+ return MIN_LON, MAX_LON
+
+def which_area(lat, lon):
+ global PREPARED_SEGMENTS, MIN_LON, MAX_LON
+
+ if not PREPARED_SEGMENTS:
+ MIN_LON, MAX_LON = generate_boundary_segments()
+
+ # Fast boundary check (East/West limits)
+ if lon < MIN_LON or lon > MAX_LON:
+ return False, "None"
+
+ # Area 1626 is North, Area 1623 is South
+ # We find the boundary latitude at this longitude
+ boundary_lat = None
+
+ for seg in PREPARED_SEGMENTS:
+ # Check if lon is within this monotonic segment
+ if seg['min_lon'] <= lon <= seg['max_lon']:
+ lons = seg['lons']
+ lats = seg['lats']
+
+ # Binary search to find the segment indices O(log n)
+ idx = bisect.bisect_right(lons, lon)
+
+ if idx == 0:
+ boundary_lat = lats[0]
+ elif idx == len(lons):
+ boundary_lat = lats[-1]
+ else:
+ # Simple Linear Interpolation (no square roots/Pythagoras)
+ # lat = y1 + (y2 - y1) * (x - x1) / (x2 - x1)
+ x1, x2 = lons[idx-1], lons[idx]
+ y1, y2 = lats[idx-1], lats[idx]
+
+ # Basic ratio calculation
+ t = (lon - x1) / (x2 - x1)
+ boundary_lat = y1 + t * (y2 - y1)
+
+ # Once we find the boundary at this lon, we compare
+ # Note: If the line doubles back, this logic uses the first segment found.
+ break
+
+ if boundary_lat is None:
+ return False, "None"
+
+ # Compare input lat to boundary lat
+ # North of boundary = 1626, South = 1623
+ area = "1626" if lat >= boundary_lat else "1623"
+ return True, area
\ No newline at end of file
diff --git a/core/test_points.gpx b/core/test_points.gpx
new file mode 100644
index 000000000..6f3df42b5
--- /dev/null
+++ b/core/test_points.gpx
@@ -0,0 +1,2 @@
+
+1626_North_TestExpected Area: 16261623_South_TestExpected Area: 16231626_North_TestExpected Area: 16261623_South_TestExpected Area: 16231626_North_TestExpected Area: 16261623_South_TestExpected Area: 16231626_North_TestExpected Area: 16261623_South_TestExpected Area: 16231626_North_TestExpected Area: 16261623_South_TestExpected Area: 16231626_North_TestExpected Area: 16261623_South_TestExpected Area: 1623
\ No newline at end of file