thinwrap_location

thinwrap_location for Erlang, Elixir and Gleam — one Hex package, a unified API across 6 providers for routing, matrix, geocoding and isochrone, with built-in polyline utilities. Zero runtime deps (OTP httpc), stateless, BYO transport.

Erlang constraints

Runtime dependencies
Zero runtime dependencies
BYO HTTP client
transport behaviour: transport => {Module, Opts}, where Module exports request/2
Provenance
Hex registry signatures + lockfile checksums
Minimum runtime
Erlang/OTP 27+
  • Truly zero dependencies — OTP only: httpc (inets + ssl) is the only HTTP surface, the stdlib json module parses JSON, and AWS SigV4 (SES, SNS, Bedrock) / JWT signing (FCM RS256, APNs ES256) are hand-rolled on crypto and public_key.
  • Hex has no build-provenance attestation like npm provenance or PEP 740: the registry signs its index and your lockfile pins each tarball checksum, which proves integrity, not where the package was built.
  • Stateless: no supervision tree, ETS tables or application env to configure. The default httpc transport is HTTPS-only and never follows redirects — a 3xx surfaces as an error rather than silently re-sending auth headers to another host.

30-second install

{deps, [{thinwrap_location, "~> 1.3"}]}.

2-minute end-to-end example

Compute a route between two waypoints through any supported provider. The examples use Mapbox; switching providers means changing the config's provider and credentials — the input and the result shape stay identical. One package, three ways to call it:

Erlang

Paste into a function body or the shell. Reads the key from MAPBOX_TOKEN. More on calling it from Erlang: /erlang.

Config = #{provider => mapbox,
           access_token => list_to_binary(os:getenv("MAPBOX_TOKEN"))},

case thinwrap_location:route(Config, #{
    waypoints => [#{lat => 37.7749, lng => -122.4194},
                  #{lat => 37.3382, lng => -121.8863}]
}) of
    {ok, #{total_distance_meters := M, total_duration_seconds := S, polyline := Polyline}} ->
        io:format("~.1f km, ~B min~n", [M / 1000, round(S / 60)]),
        io:format("~s~n", [Polyline]);  % Google precision-5 encoded
    {error, #{code := Code, message := Msg}} ->
        io:format("~s: ~ts~n", [Code, Msg])
end.

Elixir

Top-level script code (mix run). Reads the key from MAPBOX_TOKEN. More on calling it from Elixir: /elixir.

config = %{provider: :mapbox, access_token: System.fetch_env!("MAPBOX_TOKEN")}

case :thinwrap_location.route(config, %{
       waypoints: [%{lat: 37.7749, lng: -122.4194}, %{lat: 37.3382, lng: -121.8863}]
     }) do
  {:ok, %{total_distance_meters: m, total_duration_seconds: s, polyline: polyline}} ->
    IO.puts("#{Float.round(m / 1000, 1)} km, #{round(s / 60)} min")
    # Google precision-5 encoded
    IO.puts(polyline)

  {:error, %{code: code, message: message}} ->
    IO.puts("#{code}: #{message}")
end

Gleam

A complete module. Besides the package it uses gleam_erlang for atoms and envoy to read MAPBOX_TOKEN; rebar3 must be on PATH to build the package. More on calling it from Gleam: /gleam.

import envoy
import gleam/dynamic.{type Dynamic}
import gleam/dynamic/decode
import gleam/erlang/atom
import gleam/float
import gleam/int
import gleam/io
import gleam/list

@external(erlang, "thinwrap_location", "route")
fn route(config: Dynamic, input: Dynamic) -> Result(Dynamic, Dynamic)

pub fn main() -> Nil {
  let assert Ok(token) = envoy.get("MAPBOX_TOKEN")
  let config =
    obj([
      #("provider", atom("mapbox")),
      #("access_token", dynamic.string(token)),
    ])
  let input =
    obj([
      #(
        "waypoints",
        dynamic.list([at(37.7749, -122.4194), at(37.3382, -121.8863)]),
      ),
    ])

  case route(config, input) {
    Ok(res) -> {
      let summary = {
        use m <- decode.field(key("total_distance_meters"), decode.float)
        use s <- decode.field(key("total_duration_seconds"), decode.float)
        use polyline <- decode.field(key("polyline"), decode.string)
        decode.success(#(m, s, polyline))
      }
      let assert Ok(#(m, s, polyline)) = decode.run(res, summary)
      io.println(
        float.to_string(float.to_precision(m /. 1000.0, 1))
        <> " km, "
        <> int.to_string(float.round(s /. 60.0))
        <> " min",
      )
      // Google precision-5 encoded
      io.println(polyline)
    }
    Error(err) -> io.println(describe(err))
  }
}

// The library speaks atom-keyed Erlang maps; these helpers build and read them.
fn key(name: String) -> atom.Atom {
  atom.create(name)
}

fn atom(name: String) -> Dynamic {
  atom.to_dynamic(atom.create(name))
}

fn obj(fields: List(#(String, Dynamic))) -> Dynamic {
  dynamic.properties(list.map(fields, fn(f) { #(atom(f.0), f.1) }))
}

fn at(lat: Float, lng: Float) -> Dynamic {
  obj([#("lat", dynamic.float(lat)), #("lng", dynamic.float(lng))])
}

fn describe(err: Dynamic) -> String {
  let error = {
    use code <- decode.field(key("code"), atom.decoder())
    use message <- decode.field(key("message"), decode.string)
    decode.success(atom.to_string(code) <> ": " <> message)
  }
  let assert Ok(line) = decode.run(err, error)
  line
}

Same call for every routing provider — setprovider to google, here,esri, tomtom, or osrm (with abase_url). Every connector returns the same result map: meters, seconds, and a Google precision-5 polyline.

Supported providers

6 providers ship with thinwrap_location@1.3.0.
ProviderOperationsAuthREADME
google Google Maps Platformrouting, matrix, geocodingquerydocs
mapbox Mapboxrouting, matrix, geocoding, isochronequerydocs
here HERErouting, matrix, geocoding, isochroneheaderdocs
esri ESRI ArcGISrouting, matrix, geocoding, isochronebearerdocs
tomtom TomTomrouting, matrix, geocoding, isochronequerydocs
osrm OSRM (self-hosted)routing, matrixheaderdocs

Migration paths

Built-in polyline utilities

Every routing connector returns a Google precision-5 encodedpolyline. thinwrap_location_polyline decodes and re-encodes it and converts HERE's flex-polyline and ESRI's path geometry without a third-party library: encode/1,decode/1, decode_flex/1 andencode_esri_paths/1 — the same four primitives as every sibling.

Operation coverage

Provider × operation gaps below are architectural, not bugs:

  • OSRM ships only route +matrix — geocoding and isochrone are out of scope by design for OSRM. Calling thinwrap_location:geocode/2 with an OSRM config raises {badarg, <<"osrm does not support geocode">>}before any request.
  • Google does not expose a public isochrone API.

Bring your own transport

The default transport is OTP httpc: it never follows redirects and refuses cleartext http:// URLs. To reach a self-hosted OSRM over plain HTTP, or to use another HTTP client, pass a module implementing request/2 astransport => {Module, Opts}. The wrapper holds no state — the HERE/TomTom async-matrix submit-poll-retrieve cycle stays inside a single matrix call.

Cross-language siblings

The TypeScript (/packages/location-ts), PHP (/packages/location-php), Go (/packages/location-go), and Python (/packages/location-py) packages ship the same API shape and provider set — cross-language parity is a design goal, not a hard release gate.

Read more