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.
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.
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:
| Convention | Why it is that way |
|---|---|
| Degrees at every boundary | A 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 mode | A 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, clockwise | Compass 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 number | Haversine 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 once | Longitude 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. |
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
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 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 circle | Rhumb line | |
|---|---|---|
| Distance | 5 570 230 m | 5 794 129 m |
| Bearing on departure | 288.33° | 258.04° |
| Bearing on arrival | 231.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.
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.
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 | Description |
|---|---|
| Type GeoPt | GeoLat, 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. |
| Word | Description |
|---|---|
| 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. |
| Word | Description |
|---|---|
| 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. |
| Word | Description |
|---|---|
| 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. |
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.
| Word | Description |
|---|---|
| Type GeoBox | BoxSouth, 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. |
| Word | Description |
|---|---|
| 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. |
| Word | Description |
|---|---|
| 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. |
| Word | Description |
|---|---|
| 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. |
| Machine | Why | |
|---|---|---|
| ephemeris | The 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. |
| Machine | Why | |
|---|---|---|
| math | Rad and Deg at the degrees boundary, Clamp guarding every Asin and Sqr against a rounding error past ±1, Hypot, and RoundTo for the seconds. | |
| seq | Last, Taken, DropN, Append, IndexOf, and the Pair the Douglas–Peucker search carries its best candidate in. | |
| str | ToFixed and PadZero for writing a coordinate out, Trim for reading one in. | |
| julian | The 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.