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

Interop

On this page

Other systems send and expect geometries in their own formats: map libraries take GeoJSON layers, spreadsheets and other systems' APIs hold WKT text, and trackers send pairs of numbers. Jadpo reads and writes each at the edge of your app, and only there does the order of latitude and longitude matter:

Format What Jadpo does Where the axis order lives
GeoJSON geometries As Geospatial types has them: strict RFC 7946, every ring written by the right-hand rule geo.from_geojson and the wire
GeoJSON Feature, FeatureCollection Two standard generic types, Feature<G, P> (a geometry of type G and properties of type P) and FeatureCollection<G, P> (its features), read and written as RFC 7946 objects The geometry inside
GeometryCollection Still refused
WKT and EWKT geo.from_wkt(text) failing malformed or out_of_range, and geo.to_wkt(shape), longitude first as PostGIS writes it; SRID=4326; accepted, any other SRID out_of_range Those two operations
WKB The PostgreSQL driver's own encoding; no operation Inside the driver
Other coordinate systems Left out: EastingNorthing stays a value with its Crs, not converted

GeoJSON features and layers

A map library such as Leaflet or MapLibre takes a layer of features: a FeatureCollection, each Feature a geometry with properties such as a name. Jadpo has the two as standard types, so you don't declare your own.

Here is the bakery map's delivery layer. A feature's identity goes in its properties, so the bakery a patch belongs to is a property. A layer read from a mapping library goes back to it byte for byte:

example
type PatchProperties = Object {    name: PatchName    bakery_id: Bakery.id}/// The map's delivery layer, as a mapping library reads it.function patch_layer(patches: List<DeliveryPatch>) -> FeatureCollection<Polygon, PatchProperties> {    return FeatureCollection {        features: list.map(            patches,            patch -> Feature {                geometry: patch.outline                properties: PatchProperties {                    name: patch.name                    bakery_id: patch.bakery_id                }            }        )    }}/// A layer as the map is sent it.function layer_sent(layer: FeatureCollection<Polygon, PatchProperties>) -> LayerJson {    return LayerJson(json.write(layer))}/// A layer the planner drew, as its mapping library sends it.function drawn_layer(    sent: LayerJson) fails NotALayer -> FeatureCollection<Polygon, PatchProperties> {    return attempt json.read(sent, as: FeatureCollection<Polygon, PatchProperties>) {        malformed: NotALayer        mismatch: NotALayer  // an `id`, a missing geometry, altitudes on some corners only    }}
example
test "a drawn layer goes back to the map as it came" {    var sent: LayerJson = LayerJson(        "{\"type\":\"FeatureCollection\",\"features\":[{\"type\":\"Feature\",\"geometry\":{\"type\":\"Polygon\",\"coordinates\":[[[-0.2,51.49],[-0.05,51.49],[-0.05,51.54],[-0.2,51.54],[-0.2,51.49]]]},\"properties\":{\"name\":\"Soho\",\"bakery_id\":\"7b0c1f8e-3a52-4d7e-9a41-2f1d5c6e8b90\"}}]}"    )    var layer: FeatureCollection<Polygon, PatchProperties> = attempt drawn_layer(sent)    assert layer_sent(layer) == sent    assert list.count(layer.features) == 1}test "a feature's identity is in its properties" {    assert drawn_layer(        LayerJson(            "{\"type\":\"FeatureCollection\",\"features\":[{\"type\":\"Feature\",\"id\":7,\"geometry\":{\"type\":\"Polygon\",\"coordinates\":[[[-0.2,51.49],[-0.05,51.49],[-0.05,51.54],[-0.2,51.54],[-0.2,51.49]]]},\"properties\":{\"name\":\"Soho\",\"bakery_id\":\"7b0c1f8e-3a52-4d7e-9a41-2f1d5c6e8b90\"}}]}"        )    ) fails NotALayer}test "a patch with one corner at a height is refused" {    assert drawn_layer(        LayerJson(            "{\"type\":\"FeatureCollection\",\"features\":[{\"type\":\"Feature\",\"geometry\":{\"type\":\"Polygon\",\"coordinates\":[[[-0.2,51.49],[-0.05,51.49,35],[-0.05,51.54],[-0.2,51.54],[-0.2,51.49]]]},\"properties\":{\"name\":\"Soho\",\"bakery_id\":\"7b0c1f8e-3a52-4d7e-9a41-2f1d5c6e8b90\"}}]}"        )    ) fails NotALayer}
  • A feature's arguments. G is one of the six geometries or Geometry. P is an Object type of your application's, whose fields are the properties; an Input, an Output or a standard object isn't allowed. Either kind of wrong argument is refused as type.mismatch, where the type is written or where a literal's values give it. A literal takes its arguments from the type its place expects, or else from its values: a feature's from its geometry and its properties, a collection's from its features.
  • Read strictly. A feature's members are type, which must be Feature (another is variant), geometry, properties, and the bbox that RFC 7946 §5 lets any object carry. A bbox is checked as four or six numbers and ignored, as a geometry's is. An id or any other member is unknown_field: your application's identity for a feature goes in properties. A geometry of null, which RFC 7946 allows, is required, since a feature with no geometry is left out; so are properties of null or none. A collection's members are type, features and bbox. A collection of no features is read, as an empty layer, though a multi-geometry of no parts isn't. Each problem is reported at its place, such as features.0.properties.vans. A feature or collection is written in that order, type first, with no bbox.
  • No text form, and no form for people. text.from, text.parse and text.format on a feature are type.mismatch; its geometry and its properties have theirs. A feature field in an entity is stored as its GeoJSON in jsonb, as structured values are.

A route sends a layer bare, as it sends a List or Page of an Output (Output and status), so a mapping library fetches it as the whole response, plain RFC 7946 GeoJSON. Its properties hold only what an Output may, so only fields declared for clients leave the server. A layer may also be a field of an Output. OpenAPI describes it as RFC 7946's object with each feature's geometry and properties, and the API record names it, as, say, FeatureCollection<Point, PinProperties>:

example
type PinLabel = Text {    max_length: 80}type LayerJson = Text {}type PinProperties = Object {    label: PinLabel}/// The bakeries on the map, each pin labelled.action bakery_pins() -> FeatureCollection<Point, PinProperties> {    return FeatureCollection {        features: [            Feature {                geometry: Point {                    latitude: 51.5136                    longitude: -0.1365                    altitude: none                }                properties: PinProperties {                    label: PinLabel("Soho")                }            }        ]    }}routes Pins {    prefix: /pins    POST / {        authentication: RouteAuthentication.none        output: FeatureCollection<Point, PinProperties>        run: bakery_pins()    }}policy {    actions: {        bakery_pins: [Access.public]    }}test "the map fetches its pins as GeoJSON" {    var response: Response = request {        method: HttpMethod.post        path: "/pins"    }    assert response.status == 200    assert response.output == bakery_pins()    assert LayerJson(json.write(bakery_pins())) == LayerJson(        "{\"type\":\"FeatureCollection\",\"features\":[{\"type\":\"Feature\",\"geometry\":{\"type\":\"Point\",\"coordinates\":[-0.1365,51.5136]},\"properties\":{\"label\":\"Soho\"}}]}"    )}

This route answers POST, since an action builds its layer. A layer of stored records comes from a query, which a GET route runs, so a mapping library fetches it from the route's address (A query giving a map layer).

WKT and EWKT

WKT is the text form of a geometry that spreadsheets, CSV exports and other systems' APIs use, such as POINT(-0.1276 51.5072), longitude first. EWKT is PostGIS's form of it, which starts with the coordinate system, as in SRID=4326;POINT(...). WKB, the binary form, only passes between programs and databases, so it stays inside the PostgreSQL driver.

The planner's spreadsheet holds each outline as WKT:

example
type SheetText = Text {}type NotAShape = Failure {    kind: FailureKind.invalid_value}/// A patch's outline from the planner's spreadsheet, as WKT.function outline_from(sheet_text: SheetText) fails NotAShape -> Geometry {    return attempt geo.from_wkt(sheet_text) {        malformed: NotAShape  // not WKT, rings that cross, or a height on some corners only        out_of_range: NotAShape  // past the poles, or another system    }}/// An outline for the planner's spreadsheet, longitude first.function sheet_text_for(outline: Geometry) -> SheetText {    return SheetText(geo.to_wkt(outline))}
example
test "a patch comes back from the spreadsheet as it went" {    var sheet_text: SheetText = SheetText(        "POLYGON((-0.2 51.49,-0.05 51.49,-0.05 51.54,-0.2 51.54,-0.2 51.49))"    )    var outline: Geometry = attempt outline_from(sheet_text)    assert sheet_text_for(outline) == sheet_text    var respaced: Geometry = attempt outline_from(        SheetText(            "SRID=4326;polygon ((-0.2 51.49, -0.05 51.49, -0.05 51.54, -0.2 51.54, -0.2 51.49))"        )    )    assert respaced == outline}test "what isn't an outline is refused" {    assert outline_from(SheetText("POINT(-0.1276 91)")) fails NotAShape    assert outline_from(SheetText("SRID=4258;POINT(-0.1276 51.5072)")) fails NotAShape    assert outline_from(SheetText("POLYGON((0 0,10 10,0 10,10 0,0 0))")) fails NotAShape    assert outline_from(SheetText("LINESTRING(0 0,1 1 35)")) fails NotAShape}
  • geo.from_wkt gives a Geometry, the variant of the kind the text names, which your code matches, as it matches a Geometry read from GeoJSON. There is no as: to pick a kind, so you never have to say what a polygon read as a line fails with.
  • What is read. The six geometries, keywords in any case, Z written apart or joined (POINT Z (...), POINTZ(...)), a multi-point's points with or without brackets, any spacing, numbers with an exponent, and SRID=4326; before it. A position is two or three numbers, three for Z. Each is read as the double it is and held as that double's shortest decimal (Precision and determinism). A third number without Z is an altitude, as PostGIS reads it.
  • What is refused. M and ZM, EMPTY, GEOMETRYCOLLECTION, the curves and every other kind are malformed, and so are positions mixing altitudes (Altitudes are all or nothing). A coordinate out of its range is out_of_range, where PostGIS's ST_GeogFromText coerces it into range with a notice (verified, PostGIS 3.4.2). Any SRID but 4326 is out_of_range too, though PostGIS's geography takes other geographic systems (SRID=4258; is read, verified).
  • Rings are kept as written, as storage keeps them, not rewound: only GeoJSON winds rings by the right-hand rule. A polygon is checked as one read from GeoJSON is, closed rings of four points or more and rings_valid, and one that isn't valid is malformed.
  • The exact place. The reader reports the character it stopped at, counted from 0, with the condition: the start of a word that isn't a geometry, the bracket of a ring or line that isn't one, the number out of range, or the SRID's code. The operation's failure carries the condition only, as encoding.from_hex's does; the place is for the tests, and for a later failure that receives it.
  • geo.to_wkt writes ST_AsText's form: the keyword in capitals, Z before the brackets when every position has an altitude, no space after a comma, a multi-point's points each in brackets, each coordinate the decimal the value holds, and an altitude in metres with no trailing zeros. ST_AsText keeps at most 15 places after the point, so a coordinate with more (below 10⁻¹⁵ of a degree) comes back from PostGIS rounded: 0.30000000000000004 is written 0.3 (verified). Every other geometry, such as a patch with a hole, a patch wound the other way or one at a height, goes into PostGIS as WKT or EWKT and comes back exactly as written.

Pairs of numbers and GeoJSON text

A van's tracker sends its position latitude first, the map's pin longitude first, and a planner pastes a shape from a drawing tool as GeoJSON text. Each operation's name says which number comes first, so a swapped pair is a call that reads wrong, not a value that looks right:

example
type TrackerReading = Decimal {    scale: 9}type PastedText = Text {}type NotAPlace = Failure {    kind: FailureKind.invalid_value}/// Where a van's tracker says it is, latitude first.function van_at(tracker_fix: List<TrackerReading>) fails NotAPlace -> Point {    return attempt geo.from_lat_lng(tracker_fix) {        malformed: NotAPlace  // not two numbers, or three with a height        out_of_range: NotAPlace  // past a pole, or past the antimeridian    }}/// Where a pin dropped on the map is, longitude first, as GeoJSON writes it.function pin_at(pin_dropped: List<TrackerReading>) fails NotAPlace -> Point {    return attempt geo.from_lng_lat(pin_dropped) {        malformed: NotAPlace        out_of_range: NotAPlace    }}/// A patch pasted from a drawing tool, as GeoJSON.function pasted_patch(pasted: PastedText) fails NotAPlace -> Geometry {    return attempt geo.from_geojson(pasted) {        malformed: NotAPlace  // not GeoJSON, a feature, or rings that cross        out_of_range: NotAPlace  // past a pole, or past the antimeridian    }}
example
test "each pair says which way round it is" {    var van_spot: Point = attempt van_at([51.5136, -0.1365])    var map_pin: Point = attempt pin_at([-0.1365, 51.5136])    assert van_spot == map_pin    assert geo.to_wkt(map_pin) == "POINT(-0.1365 51.5136)"    assert van_spot == Point {        latitude: 51.5136        longitude: -0.1365        altitude: none    }    assert van_at([91.0, 0.0]) fails NotAPlace    var far_east: Point = attempt pin_at([91.0, 0.0])    assert far_east.longitude == 91.0    assert van_at([51.5136]) fails NotAPlace}test "a pasted patch is read longitude first" {    var patch_text: PastedText = PastedText(        "{\"type\":\"Polygon\",\"coordinates\":[[[-0.2,51.49],[-0.05,51.49],[-0.05,51.54],[-0.2,51.54],[-0.2,51.49]]]}"    )    var patch: Geometry = attempt pasted_patch(patch_text)    assert PastedText(json.write(patch)) == patch_text    assert pasted_patch(        PastedText("{\"type\":\"Point\",\"coordinates\":[-0.1365,91]}")    ) fails NotAPlace    assert pasted_patch(PastedText("POINT(-0.1365 51.5136)")) fails NotAPlace}
  • geo.from_geojson(text) gives a Geometry, the variant of the kind the text names, as geo.from_wkt does, read by the one GeoJSON reader a geometry on the wire is: strict RFC 7946, each ring closed, valid and rewound by the right-hand rule, every position's altitude given or none. Only the six geometries are read; a Feature, a FeatureCollection or a GeometryCollection is malformed, since a layer is read with json.read(text, as: FeatureCollection<G, P>). Text that isn't one JSON document or isn't one of the six is malformed, and so is a ring that isn't closed or valid, or positions of mixed sizes. A coordinate out of its range, or an altitude past a Length's, is out_of_range. The reader reports the place (coordinates.0.3) and the rule; the operation's failure carries the condition only, as geo.from_wkt's does. It takes Text only, not Bytes.
  • A pair is two numbers or three. geo.from_lng_lat(numbers) and geo.from_lat_lng(numbers) take a List of values built on Int or Decimal and give a Point. A third number is the altitude in metres, as GeoJSON's third is, a Length rounded half to even to the micrometre. Any other count is malformed. Each coordinate is first held as the shortest decimal of its nearest double, as a Point built in code is (Precision and determinism), and that value is then held to its range, as GeoJSON's numbers are. So 90.00000000000000001 is 90 and in range; past it is out_of_range. The order lives only in the name: a latitude of 91 is out_of_range from from_lat_lng and a longitude of 91 from from_lng_lat.
  • Always attempted. What a list or a text holds is known only when the program runs, so each call maps both conditions, or invalid:, even for a list written out, as geo.from_wkt does. No literal is decided while compiling.
  • No geo.to_geojson. json.write of a geometry already writes its GeoJSON, longitude first, so a pair or a text read here goes back out through it or geo.to_wkt.

Markdown for agents: /docs/geospatial-interop.md.Built from Jadpo main at a707efb.