Jadpo is experimental and not ready for production. These docs follow the compiler's main branch as of 30 Oct 2026. What's built · Install 0.1.0
jadpo
Reference: Geospatial

Geospatial

Operations

On this page

The geo. operations measure distances, lengths, directions and areas, and say whether one shape touches or holds another:

Operation Gives Fails SQL form
geo.distance(from, to) Length: the geodesic between two Points on the WGS84 ellipsoid, rounded half to even to the micrometre (Length's scale) ST_Distance(a, b)
geo.length(line) Length: a LineString's or MultiLineString's geodesics summed, each edge measured as by distance ST_Length(x)
geo.bearing(from, to) Bearing: the geodesic's direction as it leaves from, degrees clockwise from true north, at least 0 and below 360, to 9 places same_point degrees(ST_Azimuth(a, b))
geo.area(shape) Area: a Polygon's or MultiPolygon's area on the ellipsoid, holes taken away, to Area's scale ST_Area(x)
geo.intersects(a, b) Bool: the two share at least one point, edges included ST_Intersects(a, b)
geo.contains(a, b) Bool: no point of b lies outside a; a may also be a BoundingBox or a Circle ST_Covers(a, b)
geo.within(a, b) Bool: geo.contains(b, a) ST_CoveredBy(a, b)
geo.within(a, b, distance:) Bool: geo.distance(a, b) <= distance, both Points, distance a Length never below 0 ST_DWithin(a, b, d)
geo.centroid(shape) Point: the centre on the sphere, weighted by area, length or count by the shape's kind; may lie outside a concave polygon weights that cancel (two antipodal points) are a defect ST_Centroid(x, false)
geo.bounding_box(shape) BoundingBox: the smallest box of parallels and meridians holding the shape, an edge's highest or lowest point counted; across the antimeridian when that box is narrower none

Each SQL form is PostGIS's own function for the operation, which aggregate queries can use. A where: condition doesn't compile to its form: it searches a little wider and leaves the answer to Jadpo's code, as Queries explains. bounding_box and centroid have no use in a query.

Here a bakery measures its van's trips, its round and its patches:

example
type SameSpot = Failure {    kind: FailureKind.invalid_value}type VanLeg = Object {    trip_length: Length    bearing_out: Bearing}type RoundLength = Length {}type PatchArea = Area {}/// How far a bakery's van drives to a customer, and which way it sets off.function first_leg(bakery: Point, customer: Point) fails SameSpot -> VanLeg {    var setting_off: Bearing = attempt geo.bearing(bakery, customer) {        same_point: SameSpot  // a pickup at the counter    }    return VanLeg {        trip_length: geo.distance(bakery, customer)        bearing_out: setting_off    }}/// How far a van drives on its round, stop to stop.function round_length(van_round: LineString) -> RoundLength {    return RoundLength(geo.length(van_round))}/// How much ground a delivery patch covers, its holes left out.function patch_area(patch: Polygon) -> PatchArea {    return PatchArea(geo.area(patch))}

A Point literal sets every field, its altitude as none when it has none (Objects). The tests check the figures above:

example
test "London to Paris is measured on the ellipsoid" {    var big_ben: Point = Point {        latitude: 51.5072        longitude: -0.1276        altitude: none    }    var eiffel_tower: Point = Point {        latitude: 48.8566        longitude: 2.3522        altitude: none    }    assert geo.distance(big_ben, eiffel_tower) == 343896.891267metre    assert geo.distance(eiffel_tower, big_ben) == geo.distance(big_ben, eiffel_tower)    var leg: VanLeg = attempt first_leg(big_ben, eiffel_tower)    assert leg.bearing_out == 148.046085477    assert first_leg(big_ben, big_ben) fails SameSpot}test "a round is its legs summed" {    var van_round: LineString = LineString {        points: [            Point {                latitude: 51.5072                longitude: -0.1276                altitude: none            },            Point {                latitude: 48.8566                longitude: 2.3522                altitude: none            },            Point {                latitude: 51.5072                longitude: -0.1276                altitude: none            }        ]    }    assert round_length(van_round) == 687793.782534metre}

Distance and length

geo.distance agrees with GeographicLib to well under a micrometre before it rounds: it is within 75 femtometres of every distance in GeographicLib's published test set (Precision and determinism). PostGIS's doubles are good to 15 nanometres. London (51.5072° N, 0.1276° W) to Paris (48.8566° N, 2.3522° E) is 343,896.891267 m. On a sphere it would be 343,530.338 m, 367 m short (both verified with PostGIS).

geo.length adds up each edge's distance as geo.distance rounds it, so a line of two points is exactly their distance. A MultiLineString's lines are added up the same way. No distance on Earth needs more than 38 digits, and a sum that does is a defect, as it is for +.

Bearing

geo.bearing gives a direction to 9 decimal places, as an Angle keeps radians to 9. Two equal points have no direction, so a call maps the failure same_point, as a local time that doesn't exist is mapped (Zones and local times).

Two points are the same point when they have the same latitude and the same longitude, -180 being 180, or are the same pole at any longitude. Two points a picometre apart do have a direction. Whether two points are equal is known only when the program runs, so same_point is settled only by attempt, as decoding is. same_point exists only in code: ST_Azimuth gives NULL for two equal points (verified), and a query never needs a bearing.

A bearing that rounds to 360 is 0. At a pole the bearing is GeographicLib's, measured from the meridian of the longitude the pole is given with.

Area

geo.area uses Karney's method for polygons, each edge a geodesic. PostGIS's ST_Area gives 12,308,778,361.47 m² for the 1° square at the equator (verified); Jadpo gives 12,308,778,361.469453 m², and each geodesic's area matches GeographicLib's test set to the square millimetre.

It rounds once. Each ring's area, the smaller of the two sides the ring bounds, is worked out without rounding. The holes are taken away and a MultiPolygon's polygons added, and then the total is rounded half to even to the square millimetre. A hole always lies inside its outer ring (the rings_valid rule under Polygons are valid when built), so holes never outweigh it.

Centroid

geo.centroid gives a point to put a shape's label at. It isn't a measure. It is worked out on a sphere, as PostGIS's ST_Centroid(x, false) is: each point becomes a unit vector, the vectors are weighted and added up, and the sum is turned back into latitude and longitude.

It gives PostGIS's answer, quirks included:

  • each point of a MultiPoint weighs one;
  • each edge of a line gives both of its ends the edge's angle as their weight;
  • a polygon is cut into triangles, from the first corner of its first outer ring to every edge of every ring. Each triangle's centre, its corners added up, is weighted by the triangle's area, always counted positive.

So a hole adds weight rather than taking it away. A concave polygon's centroid moves with the corner its ring starts at, as PostGIS's does, and may lie outside the polygon. Verified: an L-shaped polygon's centroid is 3.214° N, 3.236° E from one starting corner and 4.151° N, 4.159° E from another.

A point's centroid is the point itself, without its altitude. When the weights cancel, as for two opposite points, the call is a defect, as PostGIS gives no number there either. The result is rounded to the nearest double, as every coordinate is.

contains, within and intersects

geo.contains(a, b) is true when no point of b lies outside a, and geo.within(a, b) is geo.contains(b, a). geo.intersects(a, b) is true when the two share at least one point, edges included. A MultiPolygon is the union of its parts, which may overlap.

The SQL form, ST_Covers on geography, compares two polygons ring by ring, so it parts from that meaning in four cases:

  • it doesn't see a hole of a that b covers: it says a square with a hole covers a square over the hole;
  • it counts rings lying along each other as crossing: it says a square doesn't cover the same square with a hole;
  • with an a of several parts, it wants all of b in one part, so two points each in a different part aren't covered.

geo.contains keeps to "no point of b lies outside a" in each of these cases, in code and in a query (Queries).

A box as the container

With a BoundingBox as a, geo.contains compares latitudes and longitudes directly, because a box's edges are parallels and meridians, not great circles. In code a box can hold any geometry; an edge's highest or lowest point, and the longitudes between its ends, count. In a query the box filter works on Point fields only (Queries).

A circle as the container

With a Circle, geo.contains is geo.within(distance:) of the circle's centre, the radius being the distance. within(distance:) measures between points only, so a circle holds points only. geo.contains(circle, shape) and geo.within(shape, circle) with anything but a Point are type.mismatch, and so is geo.within(a, b, distance:) unless both are points. The message cites the decision on point-only distance.

within a distance

geo.within(a, b, distance: d) is true when the two points are at most d apart. The distance must be proved never below 0 while compiling, as a count is, or the call is type.argument_negative. A length written out, or a value of a type with min: 0metre, is proved.

Bounding box

geo.bounding_box gives the smallest box of parallels and meridians that holds the shape. A side worked out from an edge is rounded outwards to a double, so a shape is always inside its own box. The box crosses the antimeridian when that box is narrower; when two boxes are equally narrow, it takes the one that doesn't cross.

A shape that holds a pole reaches every longitude: a point at a pole, a line through one, or a polygon around or touching one. Its box runs from -180 to 180, and only such a box holds it, whatever longitude a point at the pole is written with, since every meridian meets there.

An edge between opposite points

There is no one great circle between two opposite points of the globe, so a line's edge between them is just its two ends.

Operations Jadpo doesn't have

buffer isn't an operation (What geospatial leaves out, and what to use instead). Neither is the distance between a point and a line or polygon. It comes when one of the standard library's scenario applications needs it.

Markdown for agents: /docs/geometry-operations.md.Built from Jadpo main at a707efb.