thinwrap_location (thinwrap_location v1.3.0)

View Source

Routing, distance matrix, geocoding and isochrone across Google, Mapbox, HERE, Esri, TomTom and OSRM — one normalized surface, zero dependencies.

Every function takes a provider config and an input map and returns {ok, Result} or {error, ConnectorError}:

Config = #{provider => google, api_key => <<"...">>},
{ok, #{legs := Legs, polyline := Polyline}} =
    thinwrap_location:route(Config, #{
        waypoints => [#{lat => 40.7128, lng => -74.0060},
                      #{lat => 41.1865, lng => -73.1952}]
    }).

Two kinds of failure

  • Operation failures — a vendor error, a transport failure or timeout, request data the vendor would reject, an unsupported capability — come back as {error, connector_error()}, a plain map whose code you branch on.
  • Misuse — a missing or unknown config/input key, a value of the wrong type, an enum atom outside its set, a provider that lacks the operation — raises error({badarg, Description}). Folding those into {error, _} would make a bug in the calling code look like bad end-user input or, worse, a retryable outage.

Normalization

Distances are meters, durations seconds, coordinates #{lat, lng} (latitude first), route geometry a Google precision-5 polyline whatever the provider returned. Optional output fields are omitted from the result map when the provider gave nothing — read them with maps:get/3. Every result carries the decoded vendor body under raw (JSON null is the atom null).

Passthrough

Every input accepts passthrough => #{body => map(), headers => map(), query => map()}, forwarded verbatim and overriding what the connector set (auth included): body deep-merged, headers and query shallow-merged.

Summary

Types

Provider configuration. provider selects the connector; the credential keys depend on it

The operation-failure value. code is the normalized classification shared with the sibling libraries; status is the HTTP status when the provider answered; cause is the (credential-redacted) decoded vendor body or a redacted transport message, with a Retry-After header surfaced as <<"retryAfter">>.

Unix seconds, a UTC calendar:datetime(), or an RFC 3339 binary (<<"2026-11-02T08:00:00Z">>). An unparseable binary is invalid_request.

How much traffic a route or matrix is computed against. none (default): time-independent durations — a departure_time never adds traffic. traffic: the traffic expected at departure_time (now when absent). live: deprecated alias of traffic, kept permanently. The default is a billing decision too: on Google traffic-aware routing is a Pro-tier SKU, and HERE bills time-aware routing unless traffic is explicitly disabled.

Functions

Suggest places for the partial input (all providers except OSRM).

Forward-geocode address (all providers except OSRM).

Compute reachable-area contours around center (Mapbox, HERE, Esri, TomTom).

Compute the origins x destinations distance/duration matrix (all six providers). Cells a provider could not route are absent from cells.

Resolve a place_id from autocomplete/2 into a full candidate (all providers except OSRM). For Mapbox pass the same session_token as the preceding autocomplete: Search Box bills per session.

Reverse-geocode location (all providers except OSRM).

Compute a route through waypoints (all six providers).

Whether Provider implements Operation.

Types

autocomplete_input()

-type autocomplete_input() ::
          #{input := binary(),
            location => lat_lng(),
            radius => number(),
            language => binary(),
            session_token => binary(),
            country_filter => [binary()],
            passthrough => passthrough()}.

autocomplete_prediction()

-type autocomplete_prediction() ::
          #{description := binary(),
            place_id => binary(),
            structured_format => #{main_text := binary(), secondary_text => binary()}}.

autocomplete_result()

-type autocomplete_result() :: #{predictions := [autocomplete_prediction()], raw := term()}.

config()

-type config() ::
          #{provider := provider(),
            api_key => secret(),
            access_token => secret(),
            arcgis_token => secret(),
            base_url => binary(),
            supported_exclude_classes => [binary()],
            transport => thinwrap_location_transport:transport(),
            sleep => fun((Milliseconds :: non_neg_integer()) -> term())}.

Provider configuration. provider selects the connector; the credential keys depend on it:

  • google, here, tomtom — api_key
  • mapbox — access_token
  • esri — exactly one of api_key or arcgis_token
  • osrm — base_url (required, your own server; a path prefix is allowed) and optionally supported_exclude_classes, the exclude classes your OSRM build was compiled with ([<<"toll">>, <<"ferry">>, <<"motorway">>])

Optional on every provider: transport (see thinwrap_location_transport; default {thinwrap_location_httpc, #{}}) and sleep, the wait used between async-matrix polls (HERE always, TomTom for large grids) — a test seam.

connector_error()

-type connector_error() ::
          #{code := thinwrap_location_error:code(),
            message := binary(),
            status => integer(),
            provider_message => binary(),
            cause => term()}.

The operation-failure value. code is the normalized classification shared with the sibling libraries; status is the HTTP status when the provider answered; cause is the (credential-redacted) decoded vendor body or a redacted transport message, with a Retry-After header surfaced as <<"retryAfter">>.

departure_time()

-type departure_time() :: integer() | calendar:datetime() | binary().

Unix seconds, a UTC calendar:datetime(), or an RFC 3339 binary (<<"2026-11-02T08:00:00Z">>). An unparseable binary is invalid_request.

geocode_candidate()

-type geocode_candidate() ::
          #{formatted_address := binary(),
            location := lat_lng(),
            place_id => binary(),
            viewport => #{southwest := lat_lng(), northeast := lat_lng()}}.

geocode_input()

-type geocode_input() ::
          #{address := binary(),
            language => binary(),
            country_filter => [binary()],
            passthrough => passthrough()}.

geocode_result()

-type geocode_result() :: #{candidates := [geocode_candidate()], raw := term()}.

isochrone_contour()

-type isochrone_contour() :: #{value := float(), geometry := #{type := binary(), coordinates := list()}}.

isochrone_input()

-type isochrone_input() ::
          #{center := lat_lng(),
            type := time | distance,
            values := [number()],
            travel_mode => travel_mode(),
            departure_time => departure_time(),
            passthrough => passthrough()}.

isochrone_result()

-type isochrone_result() ::
          #{contours := [isochrone_contour()], raw := term(), meta => #{request_count := pos_integer()}}.

lat_lng()

-type lat_lng() :: #{lat := number(), lng := number()}.

matrix_cell()

-type matrix_cell() ::
          #{origin_index := non_neg_integer(),
            destination_index := non_neg_integer(),
            distance_meters := float(),
            duration_seconds := float()}.

matrix_input()

-type matrix_input() ::
          #{origins := [lat_lng()],
            destinations := [lat_lng()],
            travel_mode => travel_mode(),
            avoid_tolls => boolean(),
            departure_time => departure_time(),
            transport_mode => atom() | binary(),
            passthrough => passthrough(),
            traffic_mode => traffic_mode()}.

matrix_result()

-type matrix_result() :: #{cells := [matrix_cell()], raw := term()}.

operation()

-type operation() ::
          route | matrix | geocode | reverse_geocode | autocomplete | place_details | isochrone.

passthrough()

-type passthrough() ::
          #{body => map(),
            headers => #{binary() | atom() => binary()},
            query => #{binary() | atom() => binary() | [binary()]}}.

place_details_input()

-type place_details_input() ::
          #{place_id := binary(),
            language => binary(),
            session_token => binary(),
            include => [name],
            passthrough => passthrough()}.

place_details_result()

-type place_details_result() :: #{candidate => geocode_candidate(), name => binary(), raw := term()}.

provider()

-type provider() :: google | mapbox | here | esri | osrm | tomtom.

result(T)

-type result(T) :: {ok, T} | {error, connector_error()}.

reverse_geocode_input()

-type reverse_geocode_input() ::
          #{location := lat_lng(), language => binary(), passthrough => passthrough()}.

route_input()

-type route_input() ::
          #{waypoints := [lat_lng()],
            optimize => boolean(),
            optimize_fixed_origin => boolean(),
            optimize_fixed_destination => boolean(),
            is_round_trip => boolean(),
            departure_time => departure_time(),
            avoid_tolls => boolean(),
            avoid_ferries => boolean(),
            avoid_highways => boolean(),
            travel_mode => travel_mode(),
            transport_mode => atom() | binary(),
            passthrough => passthrough(),
            polyline_quality => simplified | detailed,
            traffic_mode => traffic_mode(),
            include => [durationWithoutTraffic]}.

route_leg()

-type route_leg() ::
          #{distance_meters := float(),
            duration_seconds := float(),
            duration_without_traffic_seconds => float()}.

route_result()

-type route_result() ::
          #{legs := [route_leg()],
            total_distance_meters := float(),
            total_duration_seconds := float(),
            polyline := binary(),
            raw := term(),
            waypoint_order => [non_neg_integer()],
            total_duration_without_traffic_seconds => float()}.

secret()

-type secret() :: binary() | fun(() -> binary()).

traffic_mode()

-type traffic_mode() :: none | traffic | live.

How much traffic a route or matrix is computed against. none (default): time-independent durations — a departure_time never adds traffic. traffic: the traffic expected at departure_time (now when absent). live: deprecated alias of traffic, kept permanently. The default is a billing decision too: on Google traffic-aware routing is a Pro-tier SKU, and HERE bills time-aware routing unless traffic is explicitly disabled.

travel_mode()

-type travel_mode() :: driving | walking | cycling.

Functions

autocomplete(Config, Input)

-spec autocomplete(config(), autocomplete_input()) -> result(autocomplete_result()).

Suggest places for the partial input (all providers except OSRM).

geocode(Config, Input)

-spec geocode(config(), geocode_input()) -> result(geocode_result()).

Forward-geocode address (all providers except OSRM).

isochrone(Config, Input)

-spec isochrone(config(), isochrone_input()) -> result(isochrone_result()).

Compute reachable-area contours around center (Mapbox, HERE, Esri, TomTom).

matrix(Config, Input)

-spec matrix(config(), matrix_input()) -> result(matrix_result()).

Compute the origins x destinations distance/duration matrix (all six providers). Cells a provider could not route are absent from cells.

place_details(Config, Input)

-spec place_details(config(), place_details_input()) -> result(place_details_result()).

Resolve a place_id from autocomplete/2 into a full candidate (all providers except OSRM). For Mapbox pass the same session_token as the preceding autocomplete: Search Box bills per session.

reverse_geocode(Config, Input)

-spec reverse_geocode(config(), reverse_geocode_input()) -> result(geocode_result()).

Reverse-geocode location (all providers except OSRM).

route(Config, Input)

-spec route(config(), route_input()) -> result(route_result()).

Compute a route through waypoints (all six providers).

supports(Provider, Operation)

-spec supports(provider(), operation()) -> boolean().

Whether Provider implements Operation.