Browse documentation
aner.geo

Geographic positions and extents

Use checked WGS84 longitude and latitude, explicit rectangular bounds, typed distances and native geographic chart observations.

Implemented

Design and implemented subset: October 17, 2026. The current native APIs require Aner 0.0.3 or newer. Map drawing and future spatial work have separate contracts.

Select geometry independently from positions

A GeoPosition stores checked WGS84 longitude/latitude, independent of installed map data. A country ID such as country.jpn, an administrative key such as CL-RM, and a position are distinct values. Select plot.map("country.jpn", ...) for Japan's outline or plot.map("chile.region.rm", ...) for just one Chilean region. Only that selected geometry is materialized; world, other countries and other Chilean regions are not dependencies. The existing chile map intentionally contains all 16 regions, even when a later .focus("CL-RM") fits its drawing to one region. Pack selection controls geometry loading; focus and extent control the viewport.

Overlaying positions does not silently assign them to countries or administrative regions. It needs an explicit spatial join or aggregation policy before reporting region level statistics. See independent country/region selection.

Decision: give a geographic position its own type

Data science needs a checked location type. Two ordinary Float64 values discard their axis order, reference system and meaning. Swapping latitude and longitude can still produce plausible numbers; multiplying a longitude by a feature scaler can still produce a number while destroying its geographic interpretation. A dedicated type catches the operations that have no geographic definition and makes serialization and distance assumptions visible.

This increment implements immutable GeoPosition and GeoExtent in aner.geo. Their coordinate system is fixed: WGS84 longitude then latitude, in decimal degrees. They integrate with typed declarations, functions, Any, casts, Optional/Result, mixed collections and notebook values. Immutable positions and extents also work as Dictionary keys and Set elements. They are ordinary native values, not new language syntax. Their public constructors validate finite Float64 coordinates and valid bounds.

The coordinate convention follows GeoJSON RFC 7946, which defines WGS84 longitude/latitude order and distinguishes position coordinates from their enclosing point, line or polygon geometry. Aner currently supports two dimensional locations. Extra coordinate components cannot implicitly become altitude, observation time or an uncertainty measure.

A geographic location is separate from the existing Point<Unit> type. Point<degC> represents an absolute temperature on an affine scale: subtracting temperatures gives a temperature interval. It has one magnitude, a unit origin and existing physical arithmetic. A location has multiple coordinate axes, reference surface and wraparound semantics. Reusing Point<m> or Point<degC> for a location would make the existing name more ambiguous and would discard essential spatial information. Point<Unit> remains unchanged.

MeaningCurrent representationWhy
An event's WGS84 longitude/latitudeGeoPositionExplicit checked geographic coordinate contract
A geographic rectangle, including a dateline crossingGeoExtentDeclared west/south/east/north bounds and wrapping
An absolute temperaturePoint<degC>Affine physical measurement
A distance or temperature intervalQuantity<m> / Quantity<delta_degC>Dimensional arithmetic and unit conversion
A state's or city's identitySource/vintage plus String GEOID or other declared codeIdentity is independent of location and boundary geometry
A municipal boundaryInstalled map pack or supplied GeoJSON polygonOne city can have many parts and holes
A station's measured temperatureTimestamp, station ID, GeoPosition, Point<degC> and quality metadataOne coordinate pair cannot contain the whole observation

Implemented API

aner
import aner.geo
import aner.units

// Illustrative city-center coordinates; not survey observations.
let boulder: GeoPosition = geo.position(longitude: -105.2705, latitude: 40.0150)
let paloAlto = geo.position(longitude: -122.1430, latitude: 37.4419)
let bounds: GeoExtent = geo.extent(west: -105.35, south: 39.95, east: -105.18, north: 40.10)
print(bounds.contains(boulder))
print(boulder.longitude())
print(boulder.latitude())
print(boulder.geojson())
let separation: Quantity<m> = boulder.great_circle_distance(paloAlto)
print(separation.to("km"))
FunctionResult and contract
geo.position(longitude: Float64, latitude: Float64)GeoPosition; longitude −180…180°, latitude −90…90°, both finite
geo.longitude(position: GeoPosition) / .longitude()Raw Float64 longitude in degrees
geo.latitude(position: GeoPosition) / .latitude()Raw Float64 latitude in degrees
geo.geojson(position: GeoPosition) / .geojson()GeoJSON Point geometry String; this is not a FeatureCollection
geo.great_circle_distance(a: GeoPosition, b: GeoPosition) / a.great_circle_distance(b)Quantity<m>; approximate shortest spherical surface distance
geo.extent(west: Float64, south: Float64, east: Float64, north: Float64)GeoExtent; nonzero longitude span, south below north, finite valid geographic bounds
geo.west(extent) / .west(), likewise south, east, northRaw Float64 bound in degrees
geo.contains(extent: GeoExtent, position: GeoPosition) / .contains(position)Inclusive rectangle membership; handles wrapped longitude

Write Float64 literals such as 40.0; Aner does not implicitly promote Int64. Use named arguments when adapting a latitude/longitude source to make the conversion reviewable. Range checking cannot detect a swapped pair when both coordinates fit both axes. Known location checks and source metadata remain necessary.

For extents, west > east deliberately crosses the antimeridian. For example, geo.extent(170.0, -20.0, -170.0, 20.0) is a narrow Pacific rectangle. geo.extent(-180.0, -90.0, 180.0, 90.0) covers the whole geographic domain. A rectangle membership test does not establish city membership: use a future point in polygon predicate on the appropriate boundary for that question.

GeoExtent also works with chart.extent(bounds). plot.geo_scatter(positions, values[, labels]) draws a List of GeoPosition values without a map pack, and chart.add_points(positions, values[, labels]) draws them over an existing geographic chart. Numeric values set point colors; a separate .point_sizes(values[, max_radius]) vector sets marker area in proportion to nonnegative values, with zero radius markers for zero. Optional String labels identify observations; the default placement avoids collisions and hides labels that do not fit. .point_labels("all") permits overlap and .point_labels("none") suppresses point text. Both point and boundary layers use the same geographic viewport and spherical display projection. See the point plotting contract and the offline example for size domains, radius limits and label placement bounds. A map projection changes the drawing. It does not mutate a GeoPosition, transform a source dataset or give an SVG coordinate a physical distance unit.

Distance has to name its method

The implemented great_circle_distance uses a sphere of radius 6,371,008.8 meters. It is useful for approximate separation and exploratory work. It is symmetric, handles the dateline and returns a physical quantity instead of an unlabelled number. It ignores altitude, terrain, roads and Earth ellipsoid flattening. It provides no accuracy guarantee for a survey, route or legal boundary. The model approximation is distinct from the positional accuracy of the inputs; extra decimal places improve neither.

A future ellipsoidal method should have a separate explicit name, declared ellipsoid, numerical guarantees and reference fixtures, including nearly antipodal positions. PROJ's geodesic documentation describes ellipsoidal forward/inverse geodesic problems and its Karney based implementation. This increment neither links PROJ nor implements those transformations.

Equality and hashing compare the exact stored coordinates, with no tolerance. In particular, longitude +180° and −180° are distinct stored values even though they refer to the same meridian; rectangle membership and distance handle that geographic alias. Longitudes at a pole also remain stored as supplied. Choose explicit geographic equivalence or rounding rules for deduplication; coordinate equality does not establish shared station identity.

There is deliberately no longitude/latitude +, subtraction, ordering, scalar multiplication or implicit conversion to a Tensor. The arithmetic mean of 179° and −179° is not a valid geographic centroid algorithm. Bounding boxes also require longitude wrapping; ordinary per axis minima and maxima can produce the wrong Pacific extent. Select a specified spherical/ellipsoidal or projected algorithm before introducing geographic centroids, buffers, areas or nearest neighbor models.

What should be represented separately

Geographic identity. Keep stable feature IDs, parent/crosswalk relationships, boundary vintage and effective dates separate from coordinates. A Boulder observation can have a position, city ID, county ID and station ID; those are different variables. A place can cross county boundaries, so a fixed one parent hierarchy is insufficient. A point at a city centroid is not the city's complete geometry, and a geocoder's approximate result is not the observed location.

Observation time and reference epoch. Measurement time, dataset publication time, boundary effective time and coordinate epoch are independent fields. A sensor can move without changing its station identity. A trajectory needs an ordered observation time and movement/gap rules in addition to positions. Future dynamic CRS support must preserve coordinate epoch, transformation grids and an area of use rather than relabelling numbers with an EPSG code.

Measurement accuracy. Store horizontal accuracy and its interpretation, vertical accuracy if present, method, quality flags and provenance. A radius must say whether it is a bound, a confidence region or an instrument estimate; anisotropic uncertainty may require covariance in a declared local frame. Unknown location and unknown uncertainty must remain explicit missing values. Rounded coordinates and a generalized boundary can change a near border classification.

Height and local frames. Altitude above an ellipsoid, orthometric elevation and depth require a vertical reference and units. A future projected position should bind easting/northing to a CRS and units, with an explicit transform into WGS84. Lab, microscope, robotics and simulation coordinates need their own frame and axis conventions, even when measured in meters. A generic Position<Frame> can be evaluated later; permitting arbitrary frame Strings before providing transformation and equality rules would make an apparently typed API unsafe.

Geometry and topology. Lines need ordered vertices; polygons need rings and hole semantics; multipolygons need distinct parts. A rectangle cannot replace these structures. Plan a separate immutable geometry family with declared reference system, dimensionality, edge model, validity and empty versus missing behavior. OGC Simple Feature Access supplies the standard vector geometry model. Spatial predicates must define boundary behavior and invalid geometry handling before they can support reliable joins.

Continuous fields. Elevation, precipitation and remote sensing data need grids rather than one object per cell. Record coordinates, axis order, cell bounds, resolution, nodata mask, units and interpolation or resampling method. Distinguish cell average measurements from point samples, and extensive totals from intensive rates before resampling. CF Conventions 1.12 provides coordinate, bounds, grid mapping and cell method conventions worth following for future scientific raster data.

Scalable data structure direction

List(GeoPosition, ...) is useful for small mixed records now. Array<GeoPosition>, NDArray<GeoPosition>, geographic Tensor arithmetic, geometry aware DataTable columns and spatial indexes are not implemented. Casting a numeric matrix to GeoPosition cannot create a coordinate contract.

The next collection should be a columnar spatial series with a shared declared CRS and validity bitmap. A point series can use contiguous Float64 longitude and latitude columns; lines and polygons also need vertex, ring and part offsets. Feature IDs and measurements should remain ordinary aligned columns. Slices should share immutable buffers, with explicit budgets for materialization and export. Spatial indexes must use the same coordinate and wraparound contract as exact predicates.

This proposed layout follows the practical direction of GeoArrow's format specification: contiguous coordinate buffers, offset arrays for nested geometry, separated or interleaved coordinate layouts, and explicit missing/empty handling. That is an interoperability target, not a claim that Aner already reads Arrow.

For persistent spatial tables, evaluate a pinned GeoParquet 1.1.0 reader/writer with explicit geometry encodings and CRS metadata, including the distinction between an absent CRS and an unknown CRS. Preserve edge interpretation and coordinate epoch. GeoJSON remains useful for small portable map inputs; repeatedly reparsing a large JSON String is an unsuitable analytical column representation. Do not silently serialize objects through JSON inside a numeric loop.

Implementation order and acceptance examples

StageCapabilityAcceptance contract
ImplementedGeoPosition, GeoExtent, checked accessors, Point GeoJSON, named spherical distance, plotting extents, value colored proportional point drawing and collision aware point labelsInvalid/nonfinite coordinates fail; dateline membership works; distance has units; symbol area is proportional to nonnegative values; points and boundaries share display coordinates; old affine points retain their behavior
NextGeometry aware table columnsSource CRS and validity survive collection/slicing with aligned feature IDs and measurement columns
NextExplicit CRS transforms and ellipsoidal measurementsReference cases agree within declared tolerances; unavailable transform grids fail clearly
NextIndexed spatial predicates, joins and aggregationcontains/covers border rules, duplicate matches, unmatched rows and candidate/exact counts are reported
LaterRaster/grid fields and time aware trajectoriesCell semantics, resampling, timestamps, gaps, units and nodata are explicit

A nearest neighbor model must record its distance metric and reference system. Euclidean distance in raw longitude/latitude can distort neighborhoods, and ordinary feature standardization does not repair that metric. Spatially related train/test records can leak information; use a validation split appropriate to the location and prediction question. Administrative averages cannot establish individual behavior. Compare sensitivity to aggregation geography and boundary vintage, and disclose positional uncertainty and small sample suppression. Fine local maps also make household or sensitive event locations easier to identify; aggregate or suppress them before chart construction when the analysis requires it.

These requirements belong to the analysis and data contracts. A more attractive projection or a tighter city viewport cannot establish the validity of a spatial inference.

See the complete callable index for the native module and method name catalogue.

Aner documentation · Reference and guides