carlos / platform Public

geo: haversine and the nearest-region pick with deterministic ties (nearest-region §6.3)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev 5fa82963ca25e595feace24522f660bd76a9ed97 parent 78c20ee
3 files changed, +134 −0
  • internal/geo/nearest.go +55 −0
  • internal/geo/nearest_test.go +56 −0
  • internal/geo/point.go +23 −0
diff --git a/internal/geo/nearest.go b/internal/geo/nearest.go
new file mode 100644
index 000000000..cdb7ac9bf
--- /dev/null
+++ b/internal/geo/nearest.go
@@ -0,0 +1,55 @@
+package geo
+
+import "math"
+
+// Candidate is one region a guess may pick. Code is in the record
+// vocabulary ("" is the deployment's home); the caller has already
+// filtered to eligible, policy-allowed candidates.
+type Candidate struct {
+ Code string
+ Home bool
+ AWSRegion string
+}
+
+// NearestWith returns the candidate nearest p, measured to each
+// candidate's AWS region through pointOf.
+//
+// - A candidate whose region has no point is left out.
+// - If the HOME is among cands and has no point, ok is false: picking
+// among the rest could choose a far region over an unmeasured near
+// home, and the home is where today's behaviour lives (spec §6.3).
+// - Distances are compared in whole kilometres. A tie goes to the home,
+// then to the lexically smallest code, so every console answers alike.
+func NearestWith(p Point, cands []Candidate, pointOf func(string) (Point, bool)) (code string, ok bool) {
+ best := -1
+ bestKm := math.MaxInt
+ for i, c := range cands {
+ cp, has := pointOf(c.AWSRegion)
+ if !has {
+ if c.Home {
+ return "", false
+ }
+ continue
+ }
+ km := int(math.Round(DistanceKm(p, cp)))
+ switch {
+ case best < 0, km < bestKm:
+ case km == bestKm && better(c, cands[best]):
+ default:
+ continue
+ }
+ best, bestKm = i, km
+ }
+ if best < 0 {
+ return "", false
+ }
+ return cands[best].Code, true
+}
+
+// better breaks an exact tie: the home first, then the smaller code.
+func better(a, b Candidate) bool {
+ if a.Home != b.Home {
+ return a.Home
+ }
+ return a.Code < b.Code
+}
diff --git a/internal/geo/nearest_test.go b/internal/geo/nearest_test.go
new file mode 100644
index 000000000..61bcb61c0
--- /dev/null
+++ b/internal/geo/nearest_test.go
@@ -0,0 +1,56 @@
+package geo
+
+import (
+ "math"
+ "testing"
+)
+
+var testPoints = map[string]Point{
+ "eu-west-1": {53.35, -6.26},
+ "eu-west-2": {51.51, -0.13},
+ "ap-southeast-2": {-33.87, 151.21},
+}
+
+func testPointOf(r string) (Point, bool) { p, ok := testPoints[r]; return p, ok }
+
+func TestDistanceKmDublinLondon(t *testing.T) {
+ d := DistanceKm(testPoints["eu-west-1"], testPoints["eu-west-2"])
+ if math.Abs(d-464) > 5 {
+ t.Fatalf("Dublin to London = %.1f km, want about 464", d)
+ }
+}
+
+func TestNearest(t *testing.T) {
+ london := Point{51.5, -0.12}
+ sydney := Point{-33.9, 151.2}
+ home := Candidate{Code: "", Home: true, AWSRegion: "eu-west-1"}
+ uk := Candidate{Code: "uk", AWSRegion: "eu-west-2"}
+ au := Candidate{Code: "au", AWSRegion: "ap-southeast-2"}
+
+ cases := []struct {
+ name string
+ p Point
+ cands []Candidate
+ want string
+ wantOK bool
+ }{
+ {"single", london, []Candidate{home}, "", true},
+ {"clear winner", london, []Candidate{home, uk, au}, "uk", true},
+ {"far side", sydney, []Candidate{home, uk, au}, "au", true},
+ {"home with no point gives no answer", london,
+ []Candidate{{Code: "", Home: true, AWSRegion: ""}, uk}, "", false},
+ {"non-home with no point is left out", london,
+ []Candidate{home, {Code: "zz", AWSRegion: "xx-nowhere-1"}}, "", true},
+ {"empty", london, nil, "", false},
+ {"tie goes to the home", testPoints["eu-west-2"],
+ []Candidate{{Code: "uk2", AWSRegion: "eu-west-2"}, {Code: "", Home: true, AWSRegion: "eu-west-2"}}, "", true},
+ {"non-home tie goes to the smaller code", testPoints["eu-west-2"],
+ []Candidate{{Code: "uk2", AWSRegion: "eu-west-2"}, {Code: "uk", AWSRegion: "eu-west-2"}}, "uk", true},
+ }
+ for _, c := range cases {
+ got, ok := NearestWith(c.p, c.cands, testPointOf)
+ if got != c.want || ok != c.wantOK {
+ t.Errorf("%s: got (%q, %v), want (%q, %v)", c.name, got, ok, c.want, c.wantOK)
+ }
+ }
+}
diff --git a/internal/geo/point.go b/internal/geo/point.go
new file mode 100644
index 000000000..e374cbf91
--- /dev/null
+++ b/internal/geo/point.go
@@ -0,0 +1,23 @@
+// Package geo turns a timezone or an IP into a point on the globe and picks
+// the nearest of a set of AWS regions (nearest-region spec §6). It holds
+// points only for the length of one call: nothing here stores, logs or
+// returns a caller's coordinates.
+package geo
+
+import "math"
+
+// Point is a latitude and longitude in degrees.
+type Point struct{ Lat, Lon float64 }
+
+// earthRadiusKm is the IUGG mean Earth radius.
+const earthRadiusKm = 6371.0088
+
+// DistanceKm is the great-circle (haversine) distance between a and b.
+func DistanceKm(a, b Point) float64 {
+ rad := func(d float64) float64 { return d * math.Pi / 180 }
+ dLat := rad(b.Lat - a.Lat)
+ dLon := rad(b.Lon - a.Lon)
+ h := math.Sin(dLat/2)*math.Sin(dLat/2) +
+ math.Cos(rad(a.Lat))*math.Cos(rad(b.Lat))*math.Sin(dLon/2)*math.Sin(dLon/2)
+ return 2 * earthRadiusKm * math.Asin(math.Sqrt(h))
+}