Sign in

    geocodec

    Location codes for MoonBit: Geohash, Open Location Code (Plus Codes), Maidenhead locators, RFC 5870 geo URIs and degrees/minutes/seconds, with a unified cell/point model carrying region, resolution and uncertainty, plus bounds, neighbourhoods, geohash covering, free-text scanning and bounded cell-to-geo-URI conversion; no runtime dependencies

    geohash
    plus-code
    open-location-code
    maidenhead
    geo-uri
    dms
    coordinate
    geospatial
    text-scanning
    Download zip
    Author
    Version
    0.5.0
    License
    Apache-2.0
    Last updated
    7 hours ago
    Downloads
    2

    No dependencies

    #geocodec

    CI

    Location codes for MoonBit: Geohash, Plus Codes, Maidenhead locators, geo: URIs and degrees/minutes/seconds.

    geocodec converts between the short codes people read, print and text. A Geohash/Plus Code/Maidenhead string denotes a cell with an extent; a decimal pair, DMS pair or geo: URI denotes a point, optionally with u=. The parser returns a Location that keeps the notation, the representative point, the exact region, the resolution, the uncertainty and the original text, so a conversion can say what it lost instead of silently returning a centre.

    let p = @geocodec.LatLng::new(39.9042, 116.4074).unwrap()
    @geocodec.geohash_encode(p, 9) // Some("wx4g0bm6c")
    @geocodec.pluscode_encode(p, code_length=10) // Some("8PFRWC34+MX")
    @geocodec.maidenhead_encode(p, precision=8) // Some("OM89EV87")

    #Coverage

    NotationEncodingDecodingBoundsExtras
    Geohash1–12 characterscentreyesneighbours, covering, precision for metres
    Plus Codes2–15 digitscentreyesshorten/recover, neighbours, padding
    Maidenhead2–10 characterscentreyesfields through extended squares
    Geo URIRFC 5870RFC 5870—altitude, uncertainty, CRS
    DMSformatparse—symbols, d/m/s, colons, hemispheres

    Plus LatLng, BoundingBox (antimeridian aware), the Location model (kind, region, resolution_m, uncertainty_m, area_m2, raw), parse_location, parse_codes for free text, describe_code for cell codes, Location::to_geouri for a bounded cell→point conversion, Location::to_cell for explicit point→cell quantisation, and geohash_cover for box coverage.

    #Install and run

    moon add sssssurf/geocodec moon run --target js cmd/main -- demo

    The CLI has the subcommands parse, geohash, plus, maidenhead, bbox, geouri, dms, scan, cover, info, convert, json and demo; json emits one line for scripts and AI agents. See README.md in the repository for recorded output, the runnable examples/basic package and the benchmarks package.

    #One model, two families

    Geohash, Plus Codes and Maidenhead denote cells. Decimal and DMS pairs and geo: URIs denote points, optionally with explicit uncertainty.

    FieldMeaning
    format / kindnotation and Cell/Point
    point / regionrepresentative point and exact cell or conservative u envelope
    resolution_mapproximate cell size, point uncertainty diameter, or 0
    uncertainty_mDouble?: None means unknown; Some(0) means explicit zero
    cell_radius_moptional geometric cell radius; independent of source accuracy
    area_m2approximate or nominal envelope area
    altitude_m / crs / rawsource altitude, CRS label and original notation

    Location::convert_to_cell(format, precision) returns a LocationConversion containing source, target, center_shift_bound_m and an optional uncertainty_bound_m. Converting a 40 m uncertain point to a 13.8 m cell does not improve its accuracy: the original 40 m survives in source, and the bound relative to the new centre is about 47.428 m. An unspecified source accuracy remains unspecified. Both to_cell and convert_to_cell propagate this state into the target, including chained conversions. uncertainty_known() is derived from the optional bound; uncertainty_bounds() returns None when no physical-position envelope is known.

    to_geouri on a decoded cell emits a covering radius rounded outwards. Converted locations use the propagated bound and omit u when accuracy is unknown. It loses rectangle shape. Point geo URIs retain their original text, including explicit u=0 and input precision. Unsupported CRSs are rejected by both geouri_parse and parse_location before any WGS84 value is returned. Location, GeoUri and LocationConversion are readonly to consumers. geouri_format preserves the validated URI, including tiny positive u.

    The full semantics and conversion-loss table are in docs/location-model.md. The resubmission response is in docs/初审补充说明.md.

    #Boundaries

    This is the notation layer only. It does not implement UTM/MGRS, ellipsoidal geodesics, datum transformations (GCJ-02/BD-09), address lookup or polygon operations; those belong to separate projection/geodesy layers.

    #Tests

    72 test blocks: published vectors, round-trips at every precision, deterministic property tests, cover checks, unified-model conversion checks and a robustness pass over malformed input. moon test passes on js and wasm-gc; CI also builds wasm and native, runs examples/basic, the CLI smoke tests and moon bench.

    #License

    Apache-2.0. Formulas come from the OLC specification, RFC 5870, the geohash description and the IARU locator scheme; no code was ported.

    BoundingBox

    pub struct BoundingBox {
    south : Double
    west : Double
    north : Double
    east : Double
    } derive(Eq)

    A rectangle in degrees. west > east means the box crosses the antimeridian; east == 180 is allowed for a box ending at the date line.

    BoundingBox::center

    fn BoundingBox::center(self : BoundingBox) -> LatLng?

    The centre of the box. For a box crossing the antimeridian the centre is normalised, so it may read -179.5 rather than 180.5.

    BoundingBox::contains

    fn BoundingBox::contains(self : BoundingBox, point : LatLng) -> Bool

    True when the point lies inside the box, antimeridian crossing included.

    BoundingBox::crosses_antimeridian

    fn BoundingBox::crosses_antimeridian(self : BoundingBox) -> Bool

    True when the box crosses the antimeridian.

    BoundingBox::east

    fn BoundingBox::east(self : BoundingBox) -> Double

    BoundingBox::equal

    fn BoundingBox::equal(BoundingBox, BoundingBox) -> Bool

    BoundingBox::height

    fn BoundingBox::height(self : BoundingBox) -> Double

    Height in degrees, always in [0, 180].

    BoundingBox::intersects

    fn BoundingBox::intersects(self : BoundingBox, other : BoundingBox) -> Bool

    True when two boxes share any area (touching edges count).

    BoundingBox::new

    fn BoundingBox::new(south : Double, west : Double, north : Double, east : Double) -> BoundingBox?

    Build a box. The latitude bounds must be ordered and inside [-90, 90]; longitudes are wrapped into [-180, 180]. Returns None for non-finite input.

    BoundingBox::north

    fn BoundingBox::north(self : BoundingBox) -> Double

    BoundingBox::not_equal

    fn BoundingBox::not_equal(x : BoundingBox, y : BoundingBox) -> Bool

    BoundingBox::south

    fn BoundingBox::south(self : BoundingBox) -> Double

    BoundingBox::to_text

    fn BoundingBox::to_text(self : BoundingBox, digits? : Int) -> String

    south,west,north,east with a fixed number of decimals.

    BoundingBox::west

    fn BoundingBox::west(self : BoundingBox) -> Double

    BoundingBox::width

    fn BoundingBox::width(self : BoundingBox) -> Double

    Width in degrees, always in [0, 360].

    GeoUri

    pub struct GeoUri {
    point : LatLng
    altitude : Double?
    uncertainty : Double?
    crs : String?
    raw : String
    } derive(Eq)

    A validated WGS84 geo: URI. Unsupported CRSs are rejected before a coordinate can escape as LatLng. Readonly outside the defining package.

    GeoUri::equal

    fn GeoUri::equal(GeoUri, GeoUri) -> Bool

    GeoUri::not_equal

    fn GeoUri::not_equal(x : GeoUri, y : GeoUri) -> Bool

    LatLng

    pub struct LatLng {
    lat : Double
    lng : Double
    } derive(Eq)

    A WGS84 latitude/longitude pair in decimal degrees.

    Values are only produced through a validating constructor, so a LatLng always has a latitude in [-90, 90] and a finite, normalised longitude.

    LatLng::equal

    fn LatLng::equal(LatLng, LatLng) -> Bool

    LatLng::lat

    fn LatLng::lat(self : LatLng) -> Double

    The latitude in degrees.

    LatLng::lng

    fn LatLng::lng(self : LatLng) -> Double

    The longitude in degrees, in [-180, 180).

    LatLng::new

    fn LatLng::new(lat : Double, lng : Double) -> LatLng?

    Build a point, or None when the latitude is outside [-90, 90] or either value is not finite. A longitude outside [-180, 180) is wrapped.

    LatLng::not_equal

    fn LatLng::not_equal(x : LatLng, y : LatLng) -> Bool

    LatLng::to_text

    fn LatLng::to_text(self : LatLng, digits? : Int) -> String

    lat, lng with a fixed number of decimals.

    Location

    pub struct Location {
    format : LocationFormat
    kind : LocationKind
    point : LatLng
    region : BoundingBox
    resolution_m : Double
    uncertainty_m : Double?
    cell_radius_m : Double?
    area_m2 : Double
    altitude_m : Double?
    crs : String?
    raw : String
    } derive(Eq)

    A parsed location with its region, resolution and uncertainty.

    Invariants:
    • region always contains point geometrically, not necessarily the physical subject when its accuracy is unknown;
    • kind == Cell means region is the exact cell, point is its centre;
    • kind == Point means the notation itself is a point; when u= is given the region is the square envelope of that uncertainty;
    • resolution_m is the longer side of the region in metres (0 when the notation carries no extent);
    • uncertainty_m is an optional coverage bound, never a zero placeholder;
    • cell_radius_m describes grid geometry, independently of input accuracy;
    • raw keeps the text that produced the value, so the original notation is never lost even when the point/region is converted further.

    Location::bounds

    fn Location::bounds(self : Location) -> BoundingBox

    Notation geometry: the exact cell, a point's u envelope, or its degenerate coordinate. For a physical-position envelope use uncertainty_bounds; neither a degenerate coordinate nor a converted cell certifies accuracy.

    Location::center

    fn Location::center(self : Location) -> LatLng

    The representative point (cell centre, or the coordinate itself).

    Location::contains

    fn Location::contains(self : Location, other : Location) -> Bool

    True when every point of other is inside this region. A cell can contain a point only when the point is inside the cell; a point can contain only a degenerate point at the same coordinate.

    Location::contains_point

    fn Location::contains_point(self : Location, point : LatLng) -> Bool

    True when the point lies inside the region.

    Location::convert_to_cell

    fn Location::convert_to_cell(self : Location, format : LocationFormat, precision : Int) -> LocationConversion?

    Location::equal

    fn Location::equal(Location, Location) -> Bool

    Location::intersects

    fn Location::intersects(self : Location, other : Location) -> Bool

    True when the two regions overlap (touching edges count).

    Location::is_cell

    fn Location::is_cell(self : Location) -> Bool

    True when the source notation denotes an area rather than a point.

    Location::is_point

    fn Location::is_point(self : Location) -> Bool

    True when the source notation denotes a point.

    Location::not_equal

    fn Location::not_equal(x : Location, y : Location) -> Bool

    Location::to_cell

    fn Location::to_cell(self : Location, format : LocationFormat, precision : Int) -> Location?

    Quantise the representative point and propagate source uncertainty. The target region is grid geometry, not a new measurement. Unknown source accuracy stays None even through repeated conversions; a known bound grows by the centre displacement and covers at least the target cell geometry. precision means characters for Geohash, significant digits for Plus Code and characters for Maidenhead.

    Location::to_geouri

    fn Location::to_geouri(self : Location) -> String

    Render a representative point with its known/propagated uncertainty. Unknown accuracy omits u, including after conversion to a grid. A freshly decoded cell carries its geometric covering radius. Rectangle shape is lost.

    Location::uncertainty_bounds

    fn Location::uncertainty_bounds(self : Location) -> BoundingBox?

    Conservative horizontal envelope of the stated/propagated uncertainty. None means no finite envelope is known. Unlike bounds, this does not mistake the coordinate or quantised grid cell for the true-position region.

    Location::uncertainty_known

    fn Location::uncertainty_known(self : Location) -> Bool

    Whether a finite coverage bound is known. Derived from the optional value.

    LocationConversion

    pub struct LocationConversion {
    source : Location
    target : Location
    center_shift_bound_m : Double
    uncertainty_bound_m : Double?
    }

    An explicit conversion record retains source geometry, text and accuracy. uncertainty_bound_m bounds the source position around the target centre; None means the accuracy is unknown or no finite bound is representable.

    LocationFormat

    pub(all) enum LocationFormat {
    GeoUri
    PlusCode
    Maidenhead
    GeoHash
    Decimal
    Dms
    } derive(Eq,
    Debug
    )

    The notation a coordinate string was written in.

    LocationFormat::equal

    LocationFormat::name

    fn LocationFormat::name(self : LocationFormat) -> String

    LocationFormat::not_equal

    fn LocationFormat::not_equal(x : LocationFormat, y : LocationFormat) -> Bool

    LocationFormat::output

    fn LocationFormat::output(self : LocationFormat, logger : &Logger) -> Unit

    LocationFormat::to_string

    fn LocationFormat::to_string(self : LocationFormat) -> String

    LocationKind

    pub(all) enum LocationKind {
    Cell
    Point
    } derive(Eq,
    Debug
    )

    Whether the source notation denotes a cell or a point.

    LocationKind::equal

    LocationKind::not_equal

    fn LocationKind::not_equal(x : LocationKind, y : LocationKind) -> Bool

    LocationKind::output

    fn LocationKind::output(self : LocationKind, logger : &Logger) -> Unit

    LocationKind::to_string

    fn LocationKind::to_string(self : LocationKind) -> String

    DEGREE

    let DEGREE : String

    A degree symbol, quoted so the source stays ASCII-only.

    DOUBLE_PRIME

    let DOUBLE_PRIME : String

    A double prime (seconds) symbol.

    GEOHASH_ALPHABET

    let GEOHASH_ALPHABET : String

    The geohash alphabet, in value order.

    GEOHASH_MAX_PRECISION

    let GEOHASH_MAX_PRECISION : Int

    The maximum number of characters a geohash can have here. Twelve characters already resolve to under a centimetre.

    PLUSCODE_ALPHABET

    let PLUSCODE_ALPHABET : String

    The OLC alphabet. 0, + and the vowels are deliberately absent.

    PLUSCODE_PADDING

    let PLUSCODE_PADDING : Char

    The padding character used by area-only codes.

    PLUSCODE_SEPARATOR

    let PLUSCODE_SEPARATOR : Char

    The character that separates the area code from the local code.

    PRIME

    let PRIME : String

    A prime (minutes) symbol.

    ascii_lower

    fn ascii_lower(s : String) -> String

    Lowercase a string using ASCII rules only.

    ascii_upper

    fn ascii_upper(s : String) -> String

    Uppercase a string using ASCII rules only.

    ascii_upper_char

    fn ascii_upper_char(c : Char) -> Char

    ASCII uppercase for the codecs' alphabets: digits, +, -, ., space and letters are the only inputs that matter.

    char_str

    fn char_str(c : Char) -> String

    A one-character string.

    chars_of

    fn chars_of(s : String) -> Array[Char]

    The characters of a string, in order.

    clamp_lat

    fn clamp_lat(lat : Double) -> Double

    Clamp a latitude into [-90, 90]. Non-finite values are returned unchanged.

    decimal_notation

    fn decimal_notation(value : Double) -> String

    Lossless Double text without an exponent, for RFC 5870 and JSON consumers. Fixed decimal rounding must not turn a tiny positive uncertainty into zero.

    describe_code

    fn describe_code(text : String) -> Location?

    Describe a Geohash, Plus Code or Maidenhead locator as a Location.

    digit_value

    fn digit_value(c : Char) -> Int?

    The digit value of an ASCII character, or None.

    format_dms

    fn format_dms(value : Double, is_lat : Bool) -> String

    Format an angle as D°M′S″H with two decimal places on the seconds.

    format_fixed

    fn format_fixed(x : Double, digits : Int) -> String

    Format a double with exactly digits digits after the decimal point. Values are rounded half away from zero. Used by the CLI and the docs so that recorded output is stable.

    format_latlng

    fn format_latlng(lat : Double, lng : Double, digits : Int) -> String

    Format a latitude/longitude pair as lat, lng.

    geohash_adjacent

    fn geohash_adjacent(code : String, dx : Int, dy : Int) -> String?

    The cell immediately dx cells east and dy cells north. The result is None when the input is not a geohash; at a pole the latitude is clamped, so a northward move from the top row can repeat the same cell.

    geohash_bounds

    fn geohash_bounds(code : String) -> BoundingBox?

    The bounding box a geohash denotes, or None when the string is not a geohash.

    geohash_cover

    fn geohash_cover(box : BoundingBox, precision : Int, max_cells? : Int) -> Array[String]?

    The geohash cells that cover a bounding box at precision. Cells are returned in ascending latitude, then ascending longitude, and the list is None when it would exceed max_cells (default 1,000). A box crossing the antimeridian produces cells on both sides of the date line.

    geohash_decode

    fn geohash_decode(code : String) -> LatLng?

    The centre of a geohash cell.

    geohash_encode

    fn geohash_encode(point : LatLng, precision : Int) -> String?

    Encode a point as a geohash of precision characters (1 to 12).

    geohash_neighbors

    fn geohash_neighbors(code : String) -> Array[String]?

    The eight neighbouring cells, ordered south-west, south, south-east, west, east, north-west, north, north-east.

    geohash_precision_for_meters

    fn geohash_precision_for_meters(meters : Double) -> Int?

    The smallest precision whose cell is no larger than meters at the equator. Returns 12 for sub-centimetre requests.

    geouri_format

    fn geouri_format(uri : GeoUri) -> String

    Preserve the validated URI exactly, including tiny u, explicit zero, coordinate/altitude precision and extension parameters. Reformatting to a fixed number of decimals could move the centre or turn a positive u into 0.

    geouri_parse

    fn geouri_parse(input : String) -> GeoUri?

    Parse a geo: URI. Returns None for a bad prefix, a coordinate outside its range, a malformed number/parameter, or an unsupported CRS.

    hemisphere_suffix

    fn hemisphere_suffix(value : Double, is_lat : Bool) -> String

    Two-letter hemisphere suffix for a coordinate value.

    index_of

    fn index_of(chars : Array[Char], needle : Char) -> Int

    The index of needle in chars, or -1.

    is_ascii

    fn is_ascii(s : String) -> Bool

    True when every character is ASCII (code point below 128).

    is_finite

    fn is_finite(x : Double) -> Bool

    True when a double is finite (not NaN, not infinite).

    lng_delta

    fn lng_delta(a : Double, b : Double) -> Double

    The signed shortest difference b - a between two longitudes, in degrees.

    maidenhead_bounds

    fn maidenhead_bounds(locator : String) -> BoundingBox?

    The bounding box a Maidenhead locator denotes.

    maidenhead_decode

    fn maidenhead_decode(locator : String) -> LatLng?

    The centre of a Maidenhead locator.

    maidenhead_encode

    fn maidenhead_encode(point : LatLng, precision? : Int) -> String?

    Encode a point as a Maidenhead locator. precision is the number of characters and must be even, from 2 to 10.

    normalize_lng

    fn normalize_lng(lng : Double) -> Double

    Normalise a longitude into [-180, 180). Non-finite values are returned unchanged so callers can test them.

    parse_codes

    fn parse_codes(text : String, max_results? : Int) -> Array[Location]

    Scan free text for up to max_results location codes. Tokens are split on whitespace and common surrounding punctuation is removed before each token is offered to parse_location. This is deliberately conservative: a token that is not a complete code is ignored. Only identical source tokens are deduplicated; co-located URIs may carry different uncertainty or altitude.

    parse_decimal

    fn parse_decimal(text : String) -> Double?

    Parse a plain decimal number without exceptions: [+-]?digits[.digits]. Coordinate text never needs an exponent, and refusing one keeps the parser total and predictable.

    parse_dms

    fn parse_dms(input : String, is_lat : Bool) -> Double?

    Parse one angle written as decimal degrees or as degrees/minutes/seconds.

    Accepted forms include 51.5, -51.5, 51 30 26.5 N, 51°30'26.5"N, 51d30m26.5sN, 51:30:26.5N and a leading sign in place of a hemisphere. is_lat selects the latitude range and the accepted hemisphere letters.

    parse_location

    fn parse_location(input : String) -> Location?

    Recognise one coordinate written as a geo URI, Plus Code, Maidenhead locator, geohash, decimal pair or DMS pair.

    pluscode_adjacent

    fn pluscode_adjacent(code : String, dx : Int, dy : Int) -> String?

    The cell immediately dx cells east and dy cells north, at the same precision. At a pole the latitude is clamped, so a northward move from the top row can repeat the same cell.

    pluscode_bounds

    fn pluscode_bounds(code : String) -> BoundingBox?

    The bounding box a full Plus Code denotes.

    pluscode_code_length

    fn pluscode_code_length(code : String) -> Int

    The number of significant characters in a full code (separator and padding removed), capped at 15.

    pluscode_decode

    fn pluscode_decode(code : String) -> LatLng?

    The centre of a full Plus Code.

    pluscode_encode

    fn pluscode_encode(point : LatLng, code_length? : Int) -> String?

    Encode a point as a Plus Code. code_length counts significant characters and excludes the separator; 10 (about 13.5 m) is the default, 11 to 15 add grid refinement, and 2/4/6/8 are the area-only lengths.

    pluscode_is_full

    fn pluscode_is_full(code : String) -> Bool

    True when the string is a valid full code: valid, not short, and with the first pair inside the latitude/longitude ranges.

    pluscode_is_short

    fn pluscode_is_short(code : String) -> Bool

    True when the string is a valid short code (the separator comes before position eight).

    pluscode_is_valid

    fn pluscode_is_valid(code : String) -> Bool

    True when the string is a syntactically valid OLC, either full or short.

    pluscode_neighbors

    fn pluscode_neighbors(code : String) -> Array[String]?

    The eight neighbouring cells, in the same order as geohash_neighbors.

    pluscode_recover

    fn pluscode_recover(short : String, reference : LatLng) -> String?

    Recover a full code from a short code, choosing the candidate nearest to reference. A full code is returned normalised.

    pluscode_shorten

    fn pluscode_shorten(code : String, reference : LatLng) -> String?

    Remove as many leading characters as the reference point safely allows. Returns the original code when the reference is not close enough, and None when the input is not a full, unpadded code of at least six digits.

    round_to

    fn round_to(x : Double, digits : Int) -> Double

    Round x to digits decimal places, half away from zero.

    split_text

    fn split_text(text : String, delimiter : Char) -> Array[String]

    Split a string on a single-character delimiter, keeping empty fields.

    split_whitespace

    fn split_whitespace(text : String) -> Array[String]

    Split on ASCII whitespace, dropping empty fields.

    string_of

    fn string_of(chars : Array[Char]) -> String

    Build a string from characters.

    to_radians

    fn to_radians(degrees : Double) -> Double

    Degrees to radians.

    trim_ascii

    fn trim_ascii(s : String) -> String

    Remove leading and trailing ASCII whitespace.

    trim_fraction

    fn trim_fraction(text : String) -> String

    Drop trailing zeros (and a trailing point) from a fixed-point rendering.