The Machines · Core numerics

geo

The Earth as arithmetic — machines/geo.shoddy

the geo machine's icon

Summary

Everything you can compute about a position on the Earth without asking anybody. How far apart two points are, and which way one lies from the other. Where you end up steering a bearing for a distance. How long a route is, how far off it you have drifted, and where two great circles cross — a great circle being the shortest path over the Earth's surface, the line a string pulled tight between two points would follow. How to write a coordinate down in the half-dozen forms people write them in. How far you can see from a given height. And — because it is the most interesting thing a latitude and a longitude know — where the sun is, and when it rises.

Pure throughout, sphere-based, and no network anywhere near it: the machine includes no networking machine, so it cannot acquire one by accident. There is no geocoding here, no tiles, no elevation and no address search. A coordinate arrives as two numbers or it does not arrive.

A Brief History of the Haversine

The distance formula at the heart of this machine is named for a navigator's shortcut. Spherical trigonometry could give the great-circle distance between two points long before anyone could afford to compute it at sea. The trouble was that the direct law-of-cosines form loses its precision badly when the angle is small — and on a voyage plotted daily, the angle is always small. The fix was to work with the versed sine, a quantity Indian astronomers had tabled centuries earlier, and specifically with half of it: the haversine. James Inman coined that name in the 1835 edition of his navigation textbook for the Royal Navy. Written in haversines, the distance formula has no catastrophic small-angle behaviour. Crucially for 1835, it could also be worked entirely from log tables. The tables are gone, but the numerical virtue is not: on IEEE doubles — the number format computers calculate in — the haversine form is still the right way to ask how far apart two nearby points are. That is why a formula named for a Victorian textbook sits in every phone's location stack — and in GeoDist.

Why It's Useful

The arithmetic is small — most of these words are three lines of trigonometry. That is exactly the argument for a machine rather than against one: small, sharp formulas that are easy to get subtly wrong are the ones worth writing down once. A mistyped haversine does not crash. It returns a plausible number, and the bug surfaces three screens later as a route that is four kilometres too long.

Six decisions are made here so that no caller has to make them:

ConventionWhy it is that way
Degrees at every boundaryA deliberate divergence from eng, which takes radians and says so. A coordinate is written in degrees everywhere on Earth — every receiver, every map, every URL. A radians surface would put a conversion at every call site, and the conversion is where the mistakes are.
Metres for every distance, and no unit modeA mode is state, this machine has none, and a distance whose unit depends on a setting made three screens ago is the worst fault of the calculators eng was compiled from. EngConvert(d, EngMetre(), EngNauticalMile()) converts at the edge.
Bearings are 0..360, north is 0, clockwiseCompass convention, not mathematical convention: geo answers a bearing a navigator can steer, not an angle from the positive x-axis. No caller ever sees −170.
The Earth is a sphere, and the error has a numberHaversine on a mean-radius sphere is within about 0.5 % of the ellipsoidal answer — a few kilometres on a transatlantic leg, a few metres across a city. Vincenty is deliberately absent; see below.
The antimeridian is handled onceLongitude wraps at ±180, and every naive midpoint, bounding box and route length breaks crossing it, usually by computing a path the long way round the planet. Callers get this right for free or the machine has failed.
Never compare two points with =Records compare structurally, so GeoPt(53.72, -1.86) = GeoPt(53.72, -1.86) is True — and one arrived at by arithmetic never will be. GeoNear is the only comparison offered, exactly as EngCNear is for complex numbers.

User's Guide

A route, which is what a list is for

A route is a List Of GeoPt and nothing else — no type, no wrapper, no builder. It is what a caller already has. Three points across northern Europe:

Include "geo.shoddy"

Def Route() As List Of GeoPt
    { GeoAt(51.5074, -0.1278),      ' London
      GeoAt(48.8566, 2.3522),       ' Paris
      GeoAt(45.7640, 4.8357) }      ' Lyon

Def Main()
    Print(Str(GeoRouteLength(Route())))         ' 735056.0073 metres
    Print(Str(Nth(GeoRouteLegs(Route()), 1)))   ' 343556.5349  London to Paris
    Print(Str(Nth(GeoRouteLegs(Route()), 2)))   ' 391499.4724  Paris to Lyon
    Print(GeoCompass(Nth(GeoRouteBearings(Route()), 1)))   ' SSE

Its enclosing box is four numbers and a flag. The flag matters: a box crossing the antimeridian has BoxWest > BoxEast, and GeoInBox needs to know rather than guess. This one does not wrap.

Let box = GeoRouteBounds(Route())
' BoxSouth 45.764   BoxNorth 51.5074   BoxWest -0.1278   BoxEast 4.8357
' BoxWraps False

How far does a fourth point lie off the route? GeoNearestOn answers all three questions a caller asks next — how far off, how far along, and which leg:

Let hit = GeoNearestOn(Route(), GeoAt(50.8503, 4.3517))    ' Brussels
' HitAway  233291.1698   metres off the route
' HitAlong 220003.8472   metres from the start
' HitLeg   1             on the London-to-Paris leg

And to draw it, sample the great circle. This is the one word here whose purpose is a picture: a great circle is a curve on every map projection, so a route drawn as two-point straight lines is visibly wrong over any distance.

GeoWaypoints(GeoAt(51.5074, -0.1278), GeoAt(45.7640, 4.8357), 5)
' the middle one is 48.6623, 2.4954 - south of the straight line between them

The sun, which is why anyone tries the machine twice

Nothing here reads a clock. The date is an argument, and that is what keeps geo out of the capability system entirely. clock is optional in every manifest, and a geo that reached for “today” would drag that optionality into a machine that is otherwise arithmetic. GeoJulian is the bridge and a caller crosses it deliberately.

Let jd = GeoJulian(2026, 6, 21)                    ' 2461212.5
Let here = GeoAt(53.72, -1.86)                     ' the Heavy Woollen District

GeoSunRise(here, jd)        ' Some(3.613168696)    03:37 UTC
GeoSunSet(here, jd)         ' Some(20.69574165)    20:42 UTC
GeoDayLength(here, jd)      ' 17.08257295          seventeen hours of it
GeoSunNoon(here, jd)        ' 12.15445517
GeoSolarDeclination(jd)     ' 23.43505541          the solstice, to two decimals
GeoEquationOfTime(jd)       ' -1.827310364         minutes

Option is the whole design here and it is not decoration. Above the Arctic circle in June the sun does not rise, because it never set. That is an ordinary answer, not an error and not a sentinel. A library returning midnight, or aborting, would be wrong in the two places the question is most interesting. GeoSunNoon is always defined, which is why it is a separate word.

GeoSunRise(GeoAt(70, 0), GeoJulian(2026, 6, 21))    ' None()  - it never set
GeoSunRise(GeoAt(70, 0), GeoJulian(2026, 12, 21))   ' None()  - it never rose
GeoDayLength(GeoAt(70, 0), GeoJulian(2026, 6, 21))  ' 24      - still a number

Times are UTC hours as a Number, 13.5 meaning 13:30. Not a string and not a clock type: this machine does no formatting and knows no time zone. A time may fall outside 0..24. That means the event happens on the UTC day either side of the one asked about — wrapping it would break the guarantee that sunrise and sunset straddle solar noon.

A rhumb line is not a great circle

A great circle is the shortest path. A rhumb line holds one compass bearing the whole way, and it is what a vessel can actually steer without continuously re-plotting. A navigation answer computed on a great circle is the wrong answer to the question that was asked. London to New York, both ways:

Great circleRhumb line
Distance5 570 230 m5 794 129 m
Bearing on departure288.33°258.04°
Bearing on arrival231.21°258.04°

Two hundred and twenty-four kilometres, and fifty-seven degrees of turn. The great circle's bearing on arrival is not its departure bearing reversed. Reversing 288.33° gives 108.33°, which is a hundred and twenty degrees wrong. That is what GeoFinalBearing exists to say.

Coordinates as text

A coordinate arrives as text far more often than as two numbers, and in more spellings than anyone expects. Everything that reads text answers Result, and every one reports the 1-based position of what it could not read.

GeoFormat(GeoAt(53.72, -1.86), 6)   ' "53.720000, -1.860000"
GeoDms(53.72, 1)                    ' 53°43'12.0"
GeoDms(-1.86, 1)                    ' -1°51'36.0"
GeoHash(GeoAt(53.72, -1.86), 6)     ' "gcw9te"
GeoLocator(GeoAt(53.72, -1.86), 6)  ' "IO93br"   the Maidenhead square

GeoParse("53.72, -1.86")            ' Ok(GeoPt(53.72, -1.86))
GeoParse("53 43 12 N, 1 51 36 W")   ' the same place
GeoParse("N53 43 12 W1 51 36")      ' and so is this
GeoParse("53.72, -1.86Q")           ' Err("... CANNOT READ 'Q' AT POSITION 13", 13)

A geohash is a box, not a point — a geohash is a short code naming one rectangle of the Earth's surface — and GeoHashBox is the word that says so. GeoFromHash answers the centre for convenience and its description names the precision it just discarded: a six-character cell is about 1.2 km by 0.6 km. This is the single commonest geohash misunderstanding and the surface is built to correct it rather than encode it. GeoHashNeighbours is what makes geohashes actually useful. Proximity search by shared prefix fails at every cell boundary, where two points a metre apart share no prefix at all. Searching the eight neighbours too is the standard repair.

Three Ceilings

Half a per cent, and it is the sphere. Every distance here is computed on a mean-radius sphere. The real Earth is an ellipsoid — a sphere slightly squashed at the poles — flattened by about a part in 300, so an answer can be out by roughly 0.5 % — a few kilometres on a transatlantic leg. Vincenty's method is the accurate one, and it is deliberately absent. It is iterative, and it fails to converge for near-antipodal points, so it would have to answer Result. It also roughly doubles the machine's conceptual weight to buy that half a per cent. Survey accuracy needs a geodesy machine, and that is a different design.

A minute of time, and it is the sun. The solar position here is the low-precision algorithm, good to about a minute of arc, so rise and set are good to about a minute of time. Ample for anything but observation. Sunrise is taken as the upper limb clearing the horizon with refraction — the standard −0.833°, named as GeoSunAltitude() so it can be seen and substituted. That is six minutes different from the centre crossing, and it is the commonest reason two implementations disagree.

Half the planet, and it is the polygon. GeoArea works by spherical excess and answers whichever of the two regions a closed ring divides the sphere into is smaller. That makes winding irrelevant, which is what a caller wants in every case but one. A polygon genuinely covering more than half the Earth comes back as its complement, and nothing here can tell the difference.

Word Reference

The type and its construction

WordDescription
Type GeoPtGeoLat, GeoLon, both degrees, latitude first because that is the order a coordinate is written in. The prefix is not tidiness: an accessor loses every naming contest, so a bare Lat would be shadowed by any local of that name anywhere.
GeoAt(lat, lon)The checked builder. Aborts naming itself on a latitude outside −90..90, a longitude outside −180..180, or a value that is not finite.
GeoTryAt(lat, lon)The total twin, answering Result, for a coordinate that came from outside. GeoAt is defined over this, so there is one set of rules and not two.
GeoWrapLon(lon)Longitude folded into (−180, 180]. Public, because a caller doing their own longitude arithmetic needs the same fold. A longitude already in range comes back bit for bit.
GeoWrapBrg(brg)A bearing folded into [0, 360). The compass twin of the above.
GeoNear(a, b, metres)The only comparison between two points this machine offers, and the reason never to use = on one.
GeoRadius()6371008.8, the IUGG mean Earth radius in metres. A word and not a literal, so the one place it is decided is visible and a caller working on another body can see what to substitute.
GeoEps()The angular tolerance below which two directions are treated as the same.

Two points

WordDescription
GeoDistance(a, b)The great-circle distance in metres, by haversine.
GeoArc(a, b)The central angle between them in radians — the quantity every other word here is really about.
GeoBearing(a, b)The initial bearing: the course to steer as you leave a.
GeoFinalBearing(a, b)The bearing on arrival, and not the initial bearing reversed.
GeoDestination(a, brg, m)The direct problem: where you end up.
GeoMidpoint(a, b)The great-circle midpoint, not the average of the coordinates. The midpoint of 179 E and 179 W is on the antimeridian, where the coordinate average is in the Gulf of Guinea.
GeoAlong(a, b, f)The point a fraction of the way. f outside 0..1 extrapolates, deliberately.
GeoAntipode(a)Diametrically opposite.
GeoEquirect(a, b)The fast flat-Earth approximation. It earns its place by being honest about being wrong: good enough to rank candidates before measuring a shortlist properly, and out by 250 km over the Atlantic. Reaching for it as a distance is a misreading.

Where two paths cross

WordDescription
GeoIntersect(a, brgA, b, brgB)Where two great circles cross, as an Option. None only when the two describe one circle — any two distinct great circles always cross. They cross twice, at antipodes; this answers the nearer.
GeoCrossTrack(p, a, b)The signed perpendicular distance from the great circle through a and b, positive to the right of the track. Signed on purpose: “two hundred metres off track” is half an answer, and an absolute value throws the other half away where a caller cannot recover it.
GeoAlongTrack(p, a, b)How far along a → b the nearest point lies. Negative behind a.
GeoClosestOnLeg(p, a, b)The foot of the perpendicular, clamped to the segment.

Routes

WordDescription
GeoRouteLength(pts)The total in metres. An empty or one-point route is 0, not an error: a route with nowhere to go has a length and it is zero.
GeoRouteLegs(pts)Each leg, n − 1 of them.
GeoRouteCumulative(pts)Distance from the start to each point, n of them, the first 0 and the last the whole length.
GeoRouteAt(pts, m)The point that far along, interpolated within its leg, as an Option. Past either end is absence, not a caller bug.
GeoRouteBearings(pts)The initial bearing of each leg — the course to steer, leg by leg.
GeoNearestOn(pts, p)The nearest point on the route, as a GeoHit: HitPoint, HitAway, HitAlong, HitLeg.
GeoRouteBounds(pts)The enclosing box, antimeridian-aware. Latitude is just the minimum and maximum; longitude is the complement of the widest gap, which is what gives a route from 179 E to 179 W a box two degrees wide instead of one spanning the planet.
GeoSimplify(pts, m)Douglas–Peucker, tolerance in metres. What makes a recorded track usable: a GPS trace is tens of thousands of points, most of them noise about a line the walker actually walked. Preserves both endpoints and never grows a route.
GeoResample(pts, m)The route re-cut at a fixed spacing — what a profile, an animation or a fair comparison between two tracks wants. The last point is always kept.
GeoWaypoints(a, b, n)n points along the great circle, both ends included. The drawing word.
GeoCentroid(pts)The spherical mean, through 3-D vectors. Averaging latitudes and longitudes puts the centroid of two points either side of the antimeridian on the wrong side of the planet, and the centroid of a ring around the pole nowhere near it.
GeoArea(pts)The spherical polygon area by spherical excess, in square metres. The ring closes itself and winding does not matter. Not the trapezoid formula, which treats each edge as straight in latitude and longitude and is out by about one per cent on a ten-degree triangle.
GeoUnitVec(p) / GeoVecPt(v) / GeoVecAdd(a, b)The 3-D vector bridge the centroid is built on.

Boxes, and cheap filtering

The box exists to make filtering cheap. Measuring the distance to every point in a large set is expensive; a box test is four comparisons. Shortlisting with a box and then measuring the shortlist properly is the standard shape, and saying so here is what stops somebody putting GeoDistance inside a Filter over ten thousand points.

WordDescription
Type GeoBoxBoxSouth, BoxNorth, BoxWest, BoxEast, BoxWraps. The flag is a field and not a derivation, because a box of one point on the antimeridian looks the same as a wrapping one.
GeoBoxAround(centre, m)The lat/lon window enclosing a radius. Named GeoBoxAround and not GeoBox because the type already publishes that name as its constructor. Near a pole it clamps latitude and widens longitude to the full circle, which is correct and surprising.
GeoInBox(p, box)Membership, antimeridian-aware. A wrapping box's test is an Or where an ordinary box's is an And, which is the one line a caller writing this by hand gets wrong.
GeoBoxUnion(x, y)The smaller of the only two arcs that can contain both.
GeoBoxCentre(box) / GeoBoxSpan(box) / GeoBoxOf(s, n, w, e)The middle going east from the west edge, the eastward span in degrees, and the builder that sets the wrap flag for you.
GeoBoundingCircle(pts)A GeoCircle covering every point: the centroid, and the distance to whichever point is furthest. Not the minimal enclosing circle, which is a harder problem with an iterative answer.

Coordinates as text

WordDescription
GeoDms(deg, places)An angle as degrees, minutes and seconds. The whole angle is rounded to seconds first and then split, because rounding the seconds last is what produces 43 minutes 60 seconds.
GeoFromDms(s)One written angle back, as Result.
GeoFormat(p, places)The paste form every mapping tool accepts.
GeoParse(s)A whole coordinate in any accepted spelling: a decimal pair; DMS with the marks; DMS in plain ASCII; N, S, E or W before or after its numbers; comma or whitespace between the halves. E or W on the first half means the caller wrote longitude first, which is rarer but unambiguous.
GeoCompass(brg) / GeoFromCompass(s)The sixteen compass points and back. Sixteen and not thirty-two: the half-winds are a nineteenth-century sailing surface and a machine offering NEbN would be answering a question nobody asks.
GeoHash(p, n)The geohash to n characters. The alphabet drops a, i, l and o.
GeoHashBox(s)The cell as a GeoBox — what a geohash is.
GeoFromHash(s)The cell's centre, for convenience, having discarded the precision.
GeoHashNeighbours(s)The eight adjacent cells, clamping at the poles and wrapping at the antimeridian.
GeoLocator(p, n) / GeoFromLocator(s)Maidenhead, IO91wm and the like, to 2, 4, 6 or 8 characters. In because it is real and delightful: amateur radio has located itself this way for forty years, and a machine that can tell a student their grid square has earned their attention in a way a haversine has not.
GeoDegSign() / GeoMinSign() / GeoSecSign()The three marks, built from their character codes rather than typed — a literal degree sign in a source file is one bad save away from mojibake.

The sun

WordDescription
GeoJulian(y, m, d)The Julian date at 00:00 UT. The bridge a caller crosses deliberately, because geo never reads a clock.
GeoSunPos(p, jd, hours)A GeoSun: SunElevation and SunAzimuth, the azimuth from north clockwise like every other bearing here.
GeoSunRise(p, jd) / GeoSunSet(p, jd)UTC hours as an Option; None inside a polar day or night.
GeoSunNoon(p, jd)Solar noon — always defined, even where rise and set are not, which is why it is separate.
GeoDayLength(p, jd)0 through 24 hours. Still a number where there is no sunrise to have.
GeoTwilight(p, jd, deg)The pair of times the sun passes a given depression: 6 civil, 12 nautical, 18 astronomical.
GeoSolarDeclination(jd)The season, as one number.
GeoEquationOfTime(jd)Why a sundial disagrees with a clock, in minutes.
GeoSunAltitude()−0.833°, the standard sunrise altitude: the upper limb clearing the horizon with refraction.

Navigation

WordDescription
GeoRhumbDistance(a, b)Constant-bearing distance.
GeoRhumbBearing(a, b)The bearing you actually steer, and it does not change along the way.
GeoRhumbDestination(a, brg, m)Where holding that bearing takes you. A rhumb line steered far enough north reaches the pole and carries on down the far side.
GeoHorizon(h)How far you can see from height h, in metres. Geometric, with no allowance for refraction — the atmosphere pushes the true horizon out by something like eight per cent, and that coefficient depends on the weather.
GeoGeographicRange(h1, h2)When a light of one height becomes visible from another: where the two horizons touch.
GeoDeadReckon(a, legs)A start and a list of bearing/distance pairs. This is Fold in one word, and it is here as much for the teaching as for the navigation.
GeoZoneOffset(lon)The nautical time zone: fifteen degrees wide, centred on its meridian. Political time zones are a database that changes several times a year and no machine should ship one; this is arithmetic, and it is genuinely what is used at sea.
GeoEcef(p, alt) / GeoFromEcef(xyz) / GeoEcefHeight(xyz)Earth-centred Cartesian, as a three-element list — the representation every satellite and every 3-D renderer wants, and the one that makes GeoCentroid obviously correct rather than surprising.

Who Uses It

MachineWhy
ephemerisThe observer is a GeoPt, and the sky's compass azimuth goes through GeoWrapBrg — the machine geo's own out-of-scope promised, built on geo's conventions.

The Machines It Uses

MachineWhy
mathRad and Deg at the degrees boundary, Clamp guarding every Asin and Sqr against a rounding error past ±1, Hypot, and RoundTo for the seconds.
seqLast, Taken, DropN, Append, IndexOf, and the Pair the Douglas–Peucker search carries its best candidate in.
strToFixed and PadZero for writing a coordinate out, Trim for reading one in.
julianThe calendar: GeoJulian is one line over JulDayNumber, so the sun words share the tree's one set of leap rules.

It does not include eng. eng duplicates Rad and Deg, drags stats, seq and str in behind it, and this machine converts nothing — a caller wanting nautical miles calls EngConvert at the edge, where the policy lives. It does not include stats either: stats publishes GeoMean, which is a geometric mean and nothing to do with geography, and a route total is a one-line Fold.