location

package module
v1.2.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 29, 2026 License: MIT Imports: 17 Imported by: 0

README

thinwrap/location (Go)

Unified, SDK-free, zero-dependency Go wrapper for routing, distance matrix, geocoding, and isochrone across Google, Mapbox, HERE, ESRI, TomTom, and OSRM. Switch vendor by changing the config type; the input and output shapes stay identical. Stateless — bring your own *http.Client, no vendor SDKs.

import location "github.com/thinwrap/location-go"

Requires Go ≥ 1.18. No third-party dependencies (standard library only).

Two-minute route

r := location.NewRouting(location.GoogleConfig{APIKey: os.Getenv("GOOGLE_MAPS_API_KEY")})

res, err := r.Route(ctx, location.RoutingOptions{
    Waypoints: []location.LatLng{
        {Lat: 40.7128, Lng: -74.0060}, // New York
        {Lat: 41.4173, Lng: -73.0001}, // Bridgeport
    },
})
if err != nil {
    var ce *location.ConnectorError
    if errors.As(err, &ce) {
        log.Printf("%s: %s", ce.ProviderCode, ce.ProviderMessage)
    }
    return
}
fmt.Printf("%.1f km, %.0f min\n", res.TotalDistanceMeters/1000, res.TotalDurationSeconds/60)

Operations & providers

The config type selects the provider. An operation a provider does not support is unrepresentable at compile time (e.g. OsrmConfig does not satisfy GeocodingConfig, so NewGeocoding(OsrmConfig{...}) will not compile).

Facade (constructor) Method(s) Providers
NewRouting Route Google, Mapbox, HERE, ESRI, OSRM, TomTom
NewMatrix Matrix Google, Mapbox, HERE, ESRI, OSRM, TomTom
NewGeocoding Geocode, ReverseGeocode, Autocomplete Google, Mapbox, HERE, ESRI, TomTom
NewIsochrone Isochrone Mapbox, HERE, ESRI, TomTom

Configs: GoogleConfig{APIKey}, MapboxConfig{AccessToken}, HereConfig{APIKey}, EsriConfig{APIKey|ArcGISToken} (exactly one), OsrmConfig{BaseURL} (required), TomTomConfig{APIKey}.

Per-provider details (endpoints, auth, error mapping, passthrough) live in docs/providers/ — one page per provider.

Routing options that cost money

Three inputs exist because the cheap thing and the correct thing are not always the same request. All three default (via their zero value) to the lean option.

Option Zero value What it changes
PolylineQuality PolylineSimplified Geometry fidelity. PolylineDetailed returned a 30x larger polyline on Mapbox and 31x on OSRM in measurement, with identical distances and durations. Honoured by Google/Mapbox/OSRM; silently ignored by HERE/TomTom/Esri.
TrafficMode TrafficNone Whether to route against live traffic. TrafficLive selects a Pro-tier SKU on Google, so it is never enabled implicitly — not even by setting DepartureTime.
Include nil Which optional output fields to fetch. Each token maps 1:1 onto one optional result field.
res, err := routing.Route(ctx, location.RoutingOptions{
    Waypoints:       waypoints,
    TrafficMode:     location.TrafficLive,                    // opt into traffic-aware routing
    PolylineQuality: location.PolylineDetailed,               // opt into full geometry
    Include:         []location.RoutingInclude{location.IncludeDurationWithoutTraffic},
})

// Non-nil only when requested AND returned natively — never synthesized, so nil
// tells you this provider did not supply it.
if res.TotalDurationWithoutTrafficSeconds != nil {
    congestion := res.TotalDurationSeconds - *res.TotalDurationWithoutTrafficSeconds
    _ = congestion
}

IncludeDurationWithoutTraffic is native on Google (staticDuration), HERE (baseDuration) and TomTom (noTrafficTravelTimeInSeconds); Mapbox, OSRM and Esri do not return it, so the field stays nil there rather than being faked.

Making OSRM's avoid-flags work

Whether OSRM accepts exclude=toll is a property of your server, not of OSRM. The same request was verified live against two builds with opposite results: the public demo build rejects it with InvalidValue, while a self-hosted instance honoured it and genuinely rerouted (138075 m / 5890 s via the toll road → 130421 m / 6513 s without).

Stock OSRM compiles no exclude classes, so the flags are rejected up front by default. If your profile was built with them, declare it:

routing := location.NewRouting(location.OsrmConfig{
    BaseURL:                 "https://routing.internal",
    SupportedExcludeClasses: []string{"toll", "ferry"},
})

Autocomplete → place details

Autocomplete returns predictions; PlaceDetails resolves one into a full candidate. "Place details" and "geocode by place id" are the same vendor call on all five providers, so this is one operation, not two — and it returns an ordinary GeocodeCandidate.

geocoding := location.NewGeocoding(location.GoogleConfig{APIKey: key})

sug, _ := geocoding.Autocomplete(ctx, location.AutocompleteOptions{Input: "blue bottle"})

// Render the usual two-line suggestion without splitting Description on a comma.
for _, p := range sug.Predictions {
    if p.StructuredFormat != nil {
        fmt.Println(p.StructuredFormat.MainText, "/", p.StructuredFormat.SecondaryText)
    } else {
        fmt.Println(p.Description)
    }
}

det, _ := geocoding.PlaceDetails(ctx, location.PlaceDetailsOptions{
    PlaceID: sug.Predictions[0].PlaceID,
})

PlaceID values are provider-scoped — a Google place id is meaningless to Mapbox.

Two things that cost money here

Google's Place Details SKU is driven by the field mask, so Name (displayName) is a Pro-tier field and only requested behind an opt-in:

geocoding.PlaceDetails(ctx, location.PlaceDetailsOptions{
    PlaceID: id,
    Include: []location.PlaceDetailsInclude{location.IncludePlaceName},
})

Note this is the opposite of Compute Routes, whose SKU is driven by request features — check per API rather than generalizing.

Mapbox Search Box bills per session, not per request. A suggest and the retrieve that follows count as one billable session only when they carry the same token, so pass the same value to both:

token := uuid()  // one per user interaction

mapbox.Autocomplete(ctx, location.AutocompleteOptions{
    Input:       input,
    Passthrough: &location.Passthrough{Query: map[string]string{"session_token": token}},
})
mapbox.PlaceDetails(ctx, location.PlaceDetailsOptions{PlaceID: id, SessionToken: token})

The wrapper cannot generate or remember that token — it holds no state.

StructuredFormat support
Provider MainText / SecondaryText
Google structuredFormat.mainText / .secondaryText — default-on, free
Mapbox name / place_formatted
HERE title / address.labelSecondaryText empty for query-type suggestions, which carry no address
TomTom poi.name / address.freeformAddressnil for street results, which have no poi.name
Esri not supported — returns a single flat text

It is never synthesized: a nil StructuredFormat means the provider gave no distinct main part, and Description remains the thing to render.

Switching providers

// Same Route(ctx, opts) call, same RoutingResult — only the config changes.
google := location.NewRouting(location.GoogleConfig{APIKey: key})
mapbox := location.NewRouting(location.MapboxConfig{AccessToken: token})
here   := location.NewRouting(location.HereConfig{APIKey: key})
osrm   := location.NewRouting(location.OsrmConfig{BaseURL: "http://localhost:5000"})

Normalized surface

Distances are meters, durations are seconds, coordinates are LatLng{Lat, Lng} (lat-first), and route geometry is a Google precision-5 polyline string. Every result carries a Raw any escape hatch holding the decoded vendor body. Isochrone contour geometry is a GeoJSON Polygon.

Bring your own HTTP client

The default client never follows redirects (a 3xx surfaces as an error rather than re-sending auth headers). Inject any HTTPClient (satisfied by *http.Client) for tracing, retries, proxying, or tests:

r := location.NewRouting(cfg, location.WithHTTPClient(myClient))

The wrapper holds no state — no caching, retries, idempotency keys, or telemetry. (The HERE/TomTom async-matrix submit/poll/retrieve cycle is transient, within a single Matrix call.)

_passthrough escape hatch

Forward vendor-specific fields the normalized input doesn't expose. Body is deep-merged into the request body; Headers/Query are shallow-merged. Consumer values win (including over connector-set values).

res, err := r.Route(ctx, location.RoutingOptions{
    Waypoints:   waypoints,
    Passthrough: &location.Passthrough{Query: map[string]string{"alternatives": "true"}},
})

Polyline utilities

Four locked, cross-language-parity helpers (stdlib-only):

location.EncodePolyline(coords)        // []LatLng -> Google precision-5 string
location.DecodePolyline(encoded)       // precision-5 string -> []LatLng
location.DecodeFlexPolyline(encoded)   // HERE flex-polyline -> []LatLng
location.EncodeEsriPaths(paths)        // [][]LatLng -> ESRI-JSON {paths, spatialReference}

Error handling

Every failure is a *ConnectorError (retrieve with errors.As) carrying a typed ProviderCode: the 6 canonical values (invalid_recipient, rate_limited, auth_failed, provider_unavailable, invalid_request, unknown) plus 7 location-extended (unsupported_field, unsupported_option, unsupported_travel_mode, profile_not_configured, matrix_polling_timeout, no_route, timeout) — byte-identical to the TypeScript, PHP and Python siblings.

no_route — "there is no route", normalized

The providers agree on nothing here. Google answers HTTP 200 with the routes key absent; HERE 200 with routes: [] plus a notices[].code; Mapbox code: "NoRoute" on either 200 or 422; OSRM the same codes on a 400; TomTom a 400 with detailedError.code; Esri a 200 whose in-body error.code: 400 names an unlocated stop in details[]. Branching on "no usable route" used to mean reimplementing all six.

In practice it almost always means a waypoint could not be matched to the road network rather than the road network is disconnected: every provider tested happily routes Reykjavik→Oslo via ferry.

timeout

Separated from provider_unavailable because it is the one transport failure a caller acts on differently — back off and retry, versus treat the provider as down.

The built-in http.Client carries a 30-second timeout; inject your own HTTPClient or pass a context with a deadline to change it. Both classify as CodeTimeout.

There is no top-level RetryAfterSeconds field: the raw Retry-After header rides in Cause (key "retryAfter") and its parsed seconds are woven into ProviderMessage.

var ce *location.ConnectorError
if errors.As(err, &ce) {
    switch ce.ProviderCode {
    case location.CodeRateLimited:      // back off
    case location.CodeAuthFailed:       // check credentials
    case location.CodeProviderUnavailable: // transient
    }
}

Security

Report vulnerabilities privately — please do not open a public issue. Preferred: a private security advisory on this repository. Alternatively, email security@thinwrap.dev. Include the affected versions and a minimal reproduction if you have one.

A vulnerability in a provider's own API or service belongs to that vendor rather than to this wrapper — please report those upstream.

MIT © Dmitry Polyanovsky

Documentation

Overview

Package location is a lightweight, SDK-free, zero-dependency Go wrapper for routing, distance matrix, geocoding, and isochrone across Google, Mapbox, HERE, ESRI, TomTom, and OSRM.

It is the Go sibling of @thinwrap/location (npm) and thinwrap/location (Packagist), sharing the same normalized surface: distances in meters, durations in seconds, coordinates as {lat, lng}, geometry as Google precision-5 polylines, and a single typed *ConnectorError.

Construct an operation facade from a provider config and call its method:

r := location.NewRouting(location.GoogleConfig{APIKey: key})
res, err := r.Route(ctx, location.RoutingOptions{
	Waypoints: []location.LatLng{{Lat: 40.71, Lng: -74.0}, {Lat: 41.42, Lng: -73.0}},
})
if err != nil {
	var ce *location.ConnectorError
	if errors.As(err, &ce) {
		// ce.ProviderCode, ce.StatusCode, ce.ProviderMessage
	}
}

The config type selects the provider; switching vendors is a one-line change (swap GoogleConfig for MapboxConfig, etc.). Operations a provider does not support are unrepresentable at compile time (e.g. OsrmConfig does not satisfy GeocodingConfig).

The wrapper holds no state: no caching, retries, idempotency keys, or telemetry. Bring your own HTTP client via WithHTTPClient; the default client never follows redirects (a 3xx surfaces as an error rather than silently re-sending auth headers to the redirect target).

Index

Constants

View Source
const MaxIsochroneValues = 4

MaxIsochroneValues is the maximum number of contour breaks supported across all four isochrone providers, set by Mapbox's native ceiling.

Variables

This section is empty.

Functions

func EncodePolyline

func EncodePolyline(coords []LatLng) (string, error)

EncodePolyline encodes coordinates into a Google-format precision-5 polyline string. It returns an *ConnectorError (invalid_request) if any coordinate is non-finite.

Types

type AutocompleteOptions

type AutocompleteOptions struct {
	Input string
	// Location and Radius bias results toward a point (ignored by some providers).
	Location *LatLng
	Radius   *float64
	Language string
	// CountryFilter is an optional hard filter of ISO 3166-1 alpha-2 codes, the
	// same vocabulary as GeocodeOptions.CountryFilter. All five geocoders support
	// one natively; each connector translates it into that vendor's parameter
	// (Google includedRegionCodes, Mapbox country, TomTom countrySet, Esri
	// countryCode, HERE in=countryCode with alpha-3 codes).
	//
	// Two provider behaviours are worth knowing. Google takes ccTLD codes rather
	// than ISO — the two disagree on the United Kingdom (GB → uk), which the
	// connector translates — and setting the filter also stops Google returning
	// *query* predictions, so it changes which kinds of suggestion come back.
	// HERE requires the filter to be accompanied by a Location.
	CountryFilter []string

	// SessionToken groups this keystroke with the rest of one user interaction,
	// closed by the PlaceDetails call carrying the SAME value.
	//
	// Google-only here: Places Autocomplete is billed per SESSION when a token
	// ties the keystroke requests to the details call that closes them, and per
	// REQUEST when it does not — so omitting it on a keystroke-driven UI multiplies
	// the bill by the number of characters typed. Google documents a v4 UUID.
	//
	// The wrapper holds no state and cannot correlate the calls, so generating and
	// threading the value is the caller's job. Ignored by every other provider.
	// (Mapbox needs the same concept but only on PlaceDetails, because its suggest
	// leg generates its own token — see PlaceDetailsOptions.SessionToken.)
	SessionToken string

	Passthrough *Passthrough
}

AutocompleteOptions is the normalized input to Geocoding.Autocomplete.

type AutocompletePrediction

type AutocompletePrediction struct {
	Description string `json:"description"`
	PlaceID     string `json:"placeId,omitempty"`

	// StructuredFormat is the prediction split into its primary and secondary
	// parts, when the provider returns them as distinct fields.
	//
	// This is what lets a UI render the usual two-line suggestion — the place name
	// above a greyed-out address — without guessing where to split Description.
	// Splitting on the first comma is the workaround this replaces, and it breaks
	// on names containing commas and on locales that order the address differently.
	//
	// Never synthesized. Non-nil only when the provider supplies a genuinely
	// distinct main part: Google (structuredFormat.mainText), Mapbox (name /
	// place_formatted), HERE (title / address.label), TomTom (poi.name /
	// address.freeformAddress — absent for street results, which have no poi.name).
	// Esri returns a single flat text field and is the genuine gap.
	//
	// So nil means "this provider/row has no distinct main part", and Description
	// remains the thing to render.
	StructuredFormat *AutocompleteStructuredFormat `json:"structuredFormat,omitempty"`
}

AutocompletePrediction is one normalized autocomplete suggestion.

type AutocompleteResult

type AutocompleteResult struct {
	Predictions []AutocompletePrediction `json:"predictions"`
	Raw         any                      `json:"raw"`
}

AutocompleteResult is the normalized autocomplete response.

type AutocompleteStructuredFormat added in v1.2.0

type AutocompleteStructuredFormat struct {
	MainText      string `json:"mainText"`
	SecondaryText string `json:"secondaryText,omitempty"`
}

AutocompleteStructuredFormat is the two display parts of a prediction.

SecondaryText is empty when the provider has a main part but no address — HERE's query-type suggestions are exactly that shape, and emitting a fabricated value there would render as a blank second line.

type ConnectorError

type ConnectorError struct {
	// StatusCode is the HTTP status, or nil for pre-response / transport failures.
	StatusCode *int
	// ProviderCode is the normalized classification.
	ProviderCode ProviderCode
	// ProviderMessage is the vendor-supplied message (or "" when absent).
	ProviderMessage string
	// Message is the human-readable error message; falls back to ProviderMessage.
	Message string
	// Cause holds the decoded vendor body (map/slice/scalar) OR an underlying error.
	Cause any
}

ConnectorError is the single typed error every operation can return. Retrieve it from a returned error with errors.As:

var ce *location.ConnectorError
if errors.As(err, &ce) { ... }

There is deliberately no top-level RetryAfterSeconds field: the raw Retry-After header rides in Cause (key "retryAfter"), and its parsed seconds are woven into ProviderMessage.

func (*ConnectorError) Error

func (e *ConnectorError) Error() string

func (*ConnectorError) Unwrap

func (e *ConnectorError) Unwrap() error

Unwrap exposes an underlying error cause (e.g. a transport error) to errors.Is / errors.As.

type EsriConfig

type EsriConfig struct {
	APIKey      string
	ArcGISToken string
}

EsriConfig authenticates the ESRI/ArcGIS provider. Provide exactly one of APIKey or ArcGISToken. Supports all four operations.

func (EsriConfig) ProviderID

func (EsriConfig) ProviderID() ProviderID

type EsriPathsGeometry

type EsriPathsGeometry struct {
	Paths            [][][]float64    `json:"paths"`
	SpatialReference SpatialReference `json:"spatialReference"`
}

EsriPathsGeometry is the ESRI-JSON `paths` geometry object ArcGIS services accept/return. Points are [lng, lat] (ESRI's lng-first convention, opposite of GeoJSON) and the spatial reference is fixed at {wkid: 4326}.

func EncodeEsriPaths

func EncodeEsriPaths(paths [][]LatLng) EsriPathsGeometry

EncodeEsriPaths converts LatLng paths into an ESRI-JSON `paths` geometry object. Each point becomes an [lng, lat] pair; the CRS is fixed at WGS-84.

type GeocodeCandidate

type GeocodeCandidate struct {
	FormattedAddress string    `json:"formattedAddress"`
	Location         LatLng    `json:"location"`
	PlaceID          string    `json:"placeId,omitempty"`
	Viewport         *Viewport `json:"viewport,omitempty"`
}

GeocodeCandidate is one normalized geocoding result.

type GeocodeOptions

type GeocodeOptions struct {
	Address string
	// Language is an optional BCP-47 language tag ("" = provider default).
	Language string
	// CountryFilter is an optional hard filter of ISO 3166-1 alpha-2 codes.
	CountryFilter []string
	Passthrough   *Passthrough
}

GeocodeOptions is the normalized input to Geocoding.Geocode.

type GeocodeResult

type GeocodeResult struct {
	Candidates []GeocodeCandidate `json:"candidates"`
	Raw        any                `json:"raw"`
}

GeocodeResult is the normalized forward-geocoding response.

type Geocoding

type Geocoding struct {
	// contains filtered or unexported fields
}

Geocoding is the geocoding facade (forward, reverse, autocomplete). Construct it with NewGeocoding.

func NewGeocoding

func NewGeocoding(cfg GeocodingConfig, opts ...Option) *Geocoding

NewGeocoding builds a geocoding facade for the provider selected by cfg's type.

func (*Geocoding) Autocomplete

func (g *Geocoding) Autocomplete(ctx context.Context, opts AutocompleteOptions) (*AutocompleteResult, error)

Autocomplete returns address predictions for a partial input.

func (*Geocoding) Geocode

func (g *Geocoding) Geocode(ctx context.Context, opts GeocodeOptions) (*GeocodeResult, error)

Geocode resolves an address to candidate locations.

func (*Geocoding) PlaceDetails added in v1.2.0

func (g *Geocoding) PlaceDetails(ctx context.Context, opts PlaceDetailsOptions) (*PlaceDetailsResult, error)

PlaceDetails resolves a PlaceID from Autocomplete into a full candidate.

All five geocoding providers support it. There is no capability check because the connector interface is unexported — every possible connector is one this package built, and the compiler already requires the method.

For Mapbox, set opts.SessionToken to the same value used for the preceding Autocomplete call: Search Box bills per session, so a matching token makes suggest+retrieve one billable session instead of two.

func (*Geocoding) ProviderID

func (g *Geocoding) ProviderID() ProviderID

ProviderID returns the provider this facade dispatches to.

func (*Geocoding) ReverseGeocode

func (g *Geocoding) ReverseGeocode(ctx context.Context, opts ReverseGeocodeOptions) (*ReverseGeocodeResult, error)

ReverseGeocode resolves a coordinate to candidate addresses.

type GeocodingConfig

type GeocodingConfig interface {
	ProviderID() ProviderID
	// contains filtered or unexported methods
}

GeocodingConfig is implemented by every provider config that supports geocoding (all except OSRM).

type GoogleConfig

type GoogleConfig struct{ APIKey string }

GoogleConfig authenticates the Google provider. Supports routing, matrix, and geocoding.

func (GoogleConfig) ProviderID

func (GoogleConfig) ProviderID() ProviderID

type HTTPClient

type HTTPClient interface {
	Do(req *http.Request) (*http.Response, error)
}

HTTPClient is the bring-your-own HTTP seam. It is satisfied by *http.Client out of the box; inject any implementation for tracing, retries, mocking, or proxying.

type HereConfig

type HereConfig struct{ APIKey string }

HereConfig authenticates the HERE provider. Supports all four operations.

func (HereConfig) ProviderID

func (HereConfig) ProviderID() ProviderID

type HereTransportMode

type HereTransportMode string

HereTransportMode is the HERE-narrowed transport mode. Set it on RoutingOptions / MatrixOptions to override the base TravelMode mapping; it is read only by the HERE connector and ignored by every other provider.

const (
	HereCar        HereTransportMode = "car"
	HereTruck      HereTransportMode = "truck"
	HerePedestrian HereTransportMode = "pedestrian"
	HereBicycle    HereTransportMode = "bicycle"
	HereScooter    HereTransportMode = "scooter"
	HereTaxi       HereTransportMode = "taxi"       // routing only
	HerePrivateBus HereTransportMode = "privateBus" // routing only
)

type Isochrone

type Isochrone struct {
	// contains filtered or unexported fields
}

Isochrone is the isochrone facade. Construct it with NewIsochrone.

func NewIsochrone

func NewIsochrone(cfg IsochroneConfig, opts ...Option) *Isochrone

NewIsochrone builds an isochrone facade for the provider selected by cfg's type.

func (*Isochrone) Isochrone

func (i *Isochrone) Isochrone(ctx context.Context, opts IsochroneOptions) (*IsochroneResult, error)

Isochrone computes reachable-area contours from a center point.

func (*Isochrone) ProviderID

func (i *Isochrone) ProviderID() ProviderID

ProviderID returns the provider this facade dispatches to.

type IsochroneConfig

type IsochroneConfig interface {
	ProviderID() ProviderID
	// contains filtered or unexported methods
}

IsochroneConfig is implemented by every provider config that supports isochrone (Mapbox, HERE, ESRI, TomTom).

type IsochroneContour

type IsochroneContour struct {
	Value    float64 `json:"value"`
	Geometry Polygon `json:"geometry"`
}

IsochroneContour is one reachable-area band.

type IsochroneMeta

type IsochroneMeta struct {
	RequestCount int `json:"requestCount"`
}

IsochroneMeta carries optional metadata, present only when more than one underlying HTTP call was made (TomTom multi-band).

type IsochroneOptions

type IsochroneOptions struct {
	Center     LatLng
	Type       IsochroneType
	Values     []float64
	TravelMode TravelMode
	// DepartureTime is an ISO 8601 string (note: a string, not time.Time,
	// matching the sibling libraries' isochrone surface).
	DepartureTime string
	Passthrough   *Passthrough
}

IsochroneOptions is the normalized input to Isochrone.Isochrone. Values holds 1..4 break values (seconds for time, meters for distance). TravelMode is driving/walking on the base surface; Mapbox and TomTom additionally accept Cycling (other providers reject it as unsupported_travel_mode).

type IsochroneResult

type IsochroneResult struct {
	Contours []IsochroneContour `json:"contours"`
	Raw      any                `json:"raw"`
	Meta     *IsochroneMeta     `json:"_meta,omitempty"`
}

IsochroneResult is the normalized isochrone response. Contours are sorted by Value ascending.

type IsochroneType

type IsochroneType string

IsochroneType selects whether Values are interpreted as time (seconds) or distance (meters).

const (
	IsochroneTime     IsochroneType = "time"
	IsochroneDistance IsochroneType = "distance"
)

type LatLng

type LatLng struct {
	Lat float64 `json:"lat"`
	Lng float64 `json:"lng"`
}

LatLng is a WGS-84 coordinate, latitude first. It is the single coordinate representation across every operation input and output.

func DecodeFlexPolyline

func DecodeFlexPolyline(encoded string) ([]LatLng, error)

DecodeFlexPolyline decodes a HERE flex-polyline encoded string. Altitude (the optional third dimension) is decoded and skipped. Reference: https://github.com/heremaps/flexible-polyline

func DecodePolyline

func DecodePolyline(encoded string) ([]LatLng, error)

DecodePolyline decodes a Google-format precision-5 polyline string.

type MapboxConfig

type MapboxConfig struct{ AccessToken string }

MapboxConfig authenticates the Mapbox provider. Supports all four operations.

func (MapboxConfig) ProviderID

func (MapboxConfig) ProviderID() ProviderID

type Matrix

type Matrix struct {
	// contains filtered or unexported fields
}

Matrix is the distance-matrix facade. Construct it with NewMatrix.

func NewMatrix

func NewMatrix(cfg MatrixConfig, opts ...Option) *Matrix

NewMatrix builds a matrix facade for the provider selected by cfg's type.

func (*Matrix) Matrix

func (m *Matrix) Matrix(ctx context.Context, opts MatrixOptions) (*MatrixResult, error)

Matrix computes an origins×destinations distance/duration matrix.

func (*Matrix) ProviderID

func (m *Matrix) ProviderID() ProviderID

ProviderID returns the provider this facade dispatches to.

type MatrixCell

type MatrixCell struct {
	OriginIndex      int     `json:"originIndex"`
	DestinationIndex int     `json:"destinationIndex"`
	DistanceMeters   float64 `json:"distanceMeters"`
	DurationSeconds  float64 `json:"durationSeconds"`
}

MatrixCell is one origin×destination pairing. Results are a flat list of cells; re-pivot into a 2D grid via OriginIndex / DestinationIndex.

type MatrixConfig

type MatrixConfig interface {
	ProviderID() ProviderID
	// contains filtered or unexported methods
}

MatrixConfig is implemented by every provider config that supports matrix.

type MatrixOptions

type MatrixOptions struct {
	Origins       []LatLng
	Destinations  []LatLng
	TravelMode    TravelMode
	AvoidTolls    bool
	DepartureTime *time.Time
	// TransportMode is a HERE-only augmentation (see RoutingOptions).
	TransportMode HereTransportMode
	Passthrough   *Passthrough

	// TrafficMode selects whether cells are computed against live traffic. The
	// zero value means no traffic.
	//
	// Opt-in for the same reason as on routing — and the stakes are higher here.
	// Google's Route Matrix bills PER ELEMENT, so a traffic-aware 10x10 request
	// moves 100 billed elements onto the Pro-tier SKU, not one. Setting
	// DepartureTime alone therefore does NOT enable traffic; ask for it.
	TrafficMode TrafficMode
}

MatrixOptions is the normalized input to Matrix.Matrix. TravelMode defaults to Driving when empty.

type MatrixResult

type MatrixResult struct {
	Cells []MatrixCell `json:"cells"`
	Raw   any          `json:"raw"`
}

MatrixResult is the normalized distance matrix.

type Option

type Option func(*facadeOptions)

Option configures a facade constructor.

func WithHTTPClient

func WithHTTPClient(c HTTPClient) Option

WithHTTPClient injects a custom HTTP client (see HTTPClient). Without it, a non-redirect-following default is used.

type OsrmConfig

type OsrmConfig struct {
	BaseURL string

	// SupportedExcludeClasses lists the exclude classes THIS OSRM build accepts —
	// "toll", "ferry", "motorway".
	//
	// Whether a class is accepted is a property of the OPERATOR'S SERVER, not of
	// OSRM, which is why it has to be declared rather than assumed. Verified live on
	// two builds that answer the same request differently: exclude=toll is rejected
	// with InvalidValue by the public demo build, and honoured by a self-hosted
	// instance where it genuinely changed the route (138075 m / 5890 s via the toll
	// road → 130421 m / 6513 s without it).
	//
	// Stock OSRM compiles no exclude classes, so by default the connector rejects
	// AvoidTolls / AvoidFerries / AvoidHighways up front with unsupported_option —
	// better than sending a request the server will bounce with an opaque
	// InvalidValue. List the classes your profile was built with and the matching
	// avoid-flags start working.
	//
	// Declared rather than probed because there is no way to ask an OSRM server what
	// it supports without issuing a request that fails, and the wrapper holds no
	// state to cache such a probe in.
	SupportedExcludeClasses []string
}

OsrmConfig points at a self-hosted OSRM instance. BaseURL is required (there is no public OSRM host). Supports routing and matrix only.

func (OsrmConfig) ProviderID

func (OsrmConfig) ProviderID() ProviderID

type Passthrough

type Passthrough struct {
	Body    map[string]any    `json:"body,omitempty"`
	Headers map[string]string `json:"headers,omitempty"`
	Query   map[string]string `json:"query,omitempty"`
}

Passthrough is the per-operation input escape hatch. Keys are forwarded verbatim: Body is deep-merged into the connector's request body, Headers and Query are shallow-merged. Consumer values intentionally OVERRIDE connector-set values (including auth) — there is deliberately no reserved-key protection.

type PlaceDetailsInclude added in v1.2.0

type PlaceDetailsInclude string

PlaceDetailsInclude is an opt-in token for an optional place-details output field, mirroring RoutingInclude.

const (
	// IncludePlaceName populates PlaceDetailsResult.Name.
	//
	// On Google this adds displayName to the MANDATORY field mask, which selects the
	// Pro SKU tier — the reason it is opt-in rather than always requested. Free on
	// HERE/Mapbox/TomTom; unavailable on Esri.
	IncludePlaceName PlaceDetailsInclude = "name"
)

type PlaceDetailsOptions added in v1.2.0

type PlaceDetailsOptions struct {
	PlaceID  string
	Language string

	// SessionToken closes a billable session opened by Autocomplete. Honoured by
	// Mapbox and Google; ignored by every other provider.
	//
	// Both vendors bill the autocomplete leg per *session* rather than per request,
	// and a session is only one session when every call carries the SAME token:
	//   - Mapbox: a suggest call plus the retrieve that follows count as ONE
	//     billable Search Box session. Sent as `session_token`.
	//   - Google: the keystroke requests plus the details call that closes them
	//     count as ONE session; without a token each keystroke is billed as its own
	//     request. Sent as `sessionToken`.
	//
	// Omitting it, or passing a fresh one, turns a single user interaction into
	// several billed ones. The wrapper holds no state and cannot correlate the
	// calls, so threading it is the caller's job.
	SessionToken string

	// Include names optional output fields to fetch. Empty means nothing extra.
	Include []PlaceDetailsInclude

	Passthrough *Passthrough
}

PlaceDetailsOptions is the input for a place-details lookup: resolve a PlaceID from an autocomplete prediction into a full candidate.

This is deliberately ONE operation rather than two. "Place details" and "geocode by place id" are the same vendor call — every provider resolves its own opaque id to the same address+coordinates payload — so splitting them would put two names on one request.

PlaceID must come from the SAME provider's Autocomplete: these ids are provider-scoped and not interchangeable.

type PlaceDetailsResult added in v1.2.0

type PlaceDetailsResult struct {
	Candidate GeocodeCandidate `json:"candidate"`

	// Name is the place's display name, when the provider returns one distinct from
	// the formatted address (e.g. "Blue Bottle Coffee" vs its street address).
	//
	// Empty on providers that only return an address (Esri), and on Google unless
	// IncludePlaceName was requested — its Place Details SKU tier is driven by the
	// field mask, and displayName is a Pro-tier field. Note this is the OPPOSITE of
	// Compute Routes, whose SKU is feature-driven: check per API, do not generalize.
	Name string `json:"name,omitempty"`

	Raw any `json:"raw"`
}

PlaceDetailsResult is the normalized place-details response.

It returns a full GeocodeCandidate rather than a new shape, because that is what the operation resolves to and reusing it means a caller can feed the result straight into whatever already consumes geocode candidates.

type Polygon

type Polygon struct {
	Type        string        `json:"type"`
	Coordinates [][][]float64 `json:"coordinates"`
}

Polygon is a GeoJSON Polygon (closed outer ring), the normalized isochrone contour geometry. Coordinates are [lng, lat] rings.

type PolylineQuality added in v1.2.0

type PolylineQuality string

PolylineQuality is the geometry fidelity of a returned polyline.

Simplified is the default because the difference is large and one-sided: measured on a single ~140km route, Mapbox returned 203 characters simplified versus 6146 full (30x), OSRM 155 versus 4873 (31x), and Google 883 versus 2565 (2.9x) — with every leg distance and duration byte-identical.

const (
	// PolylineSimplified is the zero value and the default.
	PolylineSimplified PolylineQuality = ""
	// PolylineDetailed asks for the vendor's full-fidelity geometry.
	PolylineDetailed PolylineQuality = "detailed"
)

type ProviderCode

type ProviderCode string

ProviderCode is the normalized, cross-provider error classification surfaced on every *ConnectorError. The values are byte-identical across the thinwrap location siblings: 6 canonical (shared across thinwrap scopes) + 7 location-extended.

const (
	// Canonical (shared across thinwrap scopes).
	CodeInvalidRecipient    ProviderCode = "invalid_recipient"
	CodeRateLimited         ProviderCode = "rate_limited"
	CodeAuthFailed          ProviderCode = "auth_failed"
	CodeProviderUnavailable ProviderCode = "provider_unavailable"
	CodeInvalidRequest      ProviderCode = "invalid_request"
	CodeUnknown             ProviderCode = "unknown"

	// Location-extended.
	CodeUnsupportedField      ProviderCode = "unsupported_field"       // e.g. OSRM rejecting departureTime (pre-flight)
	CodeUnsupportedOption     ProviderCode = "unsupported_option"      // e.g. OSRM rejecting avoid* flags (pre-flight)
	CodeUnsupportedTravelMode ProviderCode = "unsupported_travel_mode" // e.g. TomTom/ESRI Matrix rejecting cycling
	CodeProfileNotConfigured  ProviderCode = "profile_not_configured"  // OSRM missing compiled profile
	CodeMatrixPollingTimeout  ProviderCode = "matrix_polling_timeout"  // HERE/TomTom async matrix exceeded deadline

	// CodeNoRoute means the provider answered successfully but no route exists
	// between the given waypoints. Distinct from CodeInvalidRequest: the request
	// was well-formed, so this is a business outcome to branch on rather than a
	// bug to fix.
	//
	// The vendors agree on nothing here — Google answers HTTP 200 with the
	// "routes" key absent, HERE 200 with routes: [] plus a notices[] code, Mapbox
	// code: "NoRoute" on either 200 or 422, OSRM the same codes on a 400, TomTom a
	// 400 with detailedError.code, Esri a 200 with an in-body error.code: 400
	// whose details[] name an *unlocated* stop.
	//
	// In practice it means a waypoint could not be matched to the road network:
	// every provider tested routes Reykjavik->Oslo via ferry, so a genuinely
	// disconnected network is close to unreachable.
	CodeNoRoute ProviderCode = "no_route"

	// CodeTimeout means the request exceeded the client's timeout before any
	// response arrived.
	CodeTimeout ProviderCode = "timeout"
)

type ProviderID

type ProviderID string

ProviderID is the stable, user-facing identifier for a location provider. The string values are byte-identical across the thinwrap location siblings (TypeScript, PHP, Go).

const (
	Google ProviderID = "google"
	Mapbox ProviderID = "mapbox"
	Here   ProviderID = "here"
	Esri   ProviderID = "esri"
	Osrm   ProviderID = "osrm"
	TomTom ProviderID = "tomtom"
)

type ReverseGeocodeOptions

type ReverseGeocodeOptions struct {
	Location    LatLng
	Language    string
	Passthrough *Passthrough
}

ReverseGeocodeOptions is the normalized input to Geocoding.ReverseGeocode.

type ReverseGeocodeResult

type ReverseGeocodeResult struct {
	Candidates []GeocodeCandidate `json:"candidates"`
	Raw        any                `json:"raw"`
}

ReverseGeocodeResult mirrors GeocodeResult (a list, even for providers whose reverse endpoint natively returns a single result).

type Routing

type Routing struct {
	// contains filtered or unexported fields
}

Routing is the routing facade. Construct it with NewRouting.

func NewRouting

func NewRouting(cfg RoutingConfig, opts ...Option) *Routing

NewRouting builds a routing facade for the provider selected by cfg's type.

func (*Routing) ProviderID

func (r *Routing) ProviderID() ProviderID

ProviderID returns the provider this facade dispatches to.

func (*Routing) Route

func (r *Routing) Route(ctx context.Context, opts RoutingOptions) (*RoutingResult, error)

Route computes a route over the given waypoints.

type RoutingConfig

type RoutingConfig interface {
	ProviderID() ProviderID
	// contains filtered or unexported methods
}

RoutingConfig is implemented by every provider config that supports routing.

type RoutingInclude added in v1.2.0

type RoutingInclude string

RoutingInclude is an opt-in token for an optional normalized output field.

Nothing extra is fetched unless it is named. Each token maps 1:1 onto one optional field of RoutingResult / RoutingLeg — that 1:1 rule is what keeps this from degenerating into a second Passthrough. Vendor data with no normalized field gets no token; read it from Raw.

const (
	// IncludeDurationWithoutTraffic populates
	// RoutingLeg.DurationWithoutTrafficSeconds and
	// RoutingResult.TotalDurationWithoutTrafficSeconds.
	//
	// Free on Google (a field-mask entry), HERE (already inside the requested
	// summary) and Mapbox/OSRM (not returned at all); TomTom needs the extra
	// computeTravelTimeFor=all request parameter.
	IncludeDurationWithoutTraffic RoutingInclude = "durationWithoutTraffic"
)

type RoutingLeg

type RoutingLeg struct {
	DistanceMeters  float64 `json:"distanceMeters"`
	DurationSeconds float64 `json:"durationSeconds"`

	// DurationWithoutTrafficSeconds is the leg duration ignoring traffic. Non-nil
	// only when IncludeDurationWithoutTraffic was requested AND the provider
	// returned it natively — never synthesized, so nil is meaningful: this provider
	// did not supply the value for this request.
	//
	// The point of having it alongside DurationSeconds is the delta: the two
	// together say how much of a trip's time is congestion, which makes
	// ETA-vs-baseline comparisons possible without a second request.
	//
	// Native on Google (staticDuration), HERE (baseDuration) and TomTom
	// (noTrafficTravelTimeInSeconds). Mapbox, OSRM and Esri do not return it.
	DurationWithoutTrafficSeconds *float64 `json:"durationWithoutTrafficSeconds,omitempty"`
}

RoutingLeg is one segment of a route.

type RoutingOptions

type RoutingOptions struct {
	Waypoints                []LatLng
	Optimize                 bool
	OptimizeFixedOrigin      bool
	OptimizeFixedDestination bool
	IsRoundTrip              bool
	DepartureTime            *time.Time
	AvoidTolls               bool
	AvoidFerries             bool
	AvoidHighways            bool
	TravelMode               TravelMode
	// TransportMode is a HERE-only augmentation; when set it overrides the
	// TravelMode mapping. Ignored by all other providers.
	TransportMode HereTransportMode
	Passthrough   *Passthrough

	// PolylineQuality is the geometry fidelity of the returned Polyline. The zero
	// value means simplified.
	//
	// A best-effort hint, not a guarantee. Google, Mapbox and OSRM honour both
	// values. HERE and TomTom expose no fidelity control, and Esri offers only a
	// generalization *tolerance* in map units (mapping onto some chosen tolerance
	// would mean inventing a magic number), so on those three the value is silently
	// ignored. That is deliberate: fidelity is cosmetic, so extra vertices cannot
	// make a caller's result wrong — unlike AvoidTolls, whose silent omission WOULD
	// change routing semantics and therefore errors.
	PolylineQuality PolylineQuality

	// TrafficMode selects whether the route is computed against live traffic. The
	// zero value means no traffic.
	//
	// Opt-in because traffic is a billable upgrade on some providers — most
	// notably Google, where it selects a Pro-tier SKU. It is therefore NOT implied
	// by setting DepartureTime. Providers with no traffic control ignore it.
	TrafficMode TrafficMode

	// Include names optional normalized output fields to fetch. Empty means
	// nothing extra is requested, so the response stays as small and as cheap as
	// the provider allows.
	Include []RoutingInclude
}

RoutingOptions is the normalized input to Routing.Route. Waypoints must have at least two entries. TravelMode defaults to Driving when empty.

type RoutingResult

type RoutingResult struct {
	Legs                 []RoutingLeg `json:"legs"`
	TotalDistanceMeters  float64      `json:"totalDistanceMeters"`
	TotalDurationSeconds float64      `json:"totalDurationSeconds"`
	Polyline             string       `json:"polyline"`
	// WaypointOrder, when present, lists input waypoint indices in visiting
	// order (0-based, including origin and destination). Nil when no
	// optimization was requested or the vendor returned no ordering.
	WaypointOrder []int `json:"waypointOrder,omitempty"`

	// TotalDurationWithoutTrafficSeconds is the whole-route duration ignoring
	// traffic. Same contract as RoutingLeg.DurationWithoutTrafficSeconds: opt-in
	// via IncludeDurationWithoutTraffic, vendor-native only, never synthesized.
	TotalDurationWithoutTrafficSeconds *float64 `json:"totalDurationWithoutTrafficSeconds,omitempty"`

	Raw any `json:"raw"`
}

RoutingResult is the normalized route. Distances are meters, durations are seconds, and Polyline is a Google precision-5 encoded string.

type SpatialReference

type SpatialReference struct {
	Wkid int `json:"wkid"`
}

SpatialReference is the CRS wrapper in ESRI-JSON geometry. Locked to WGS-84.

type TomTomConfig

type TomTomConfig struct{ APIKey string }

TomTomConfig authenticates the TomTom provider. Supports all four operations.

func (TomTomConfig) ProviderID

func (TomTomConfig) ProviderID() ProviderID

type TrafficMode added in v1.2.0

type TrafficMode string

TrafficMode selects how much traffic data a route is computed against.

const (
	// TrafficNone is the zero value and the default. On Google this keeps the
	// request on the base Essentials SKU rather than Pro.
	TrafficNone TrafficMode = ""
	// TrafficLive uses current traffic conditions where the provider supports it.
	TrafficLive TrafficMode = "live"
)

type TravelMode

type TravelMode string

TravelMode is the normalized travel mode. Providers map it to their native vocabulary; unsupported combinations surface as unsupported_travel_mode.

const (
	Driving TravelMode = "driving"
	Walking TravelMode = "walking"
	Cycling TravelMode = "cycling"
)

type Viewport

type Viewport struct {
	Southwest LatLng `json:"southwest"`
	Northeast LatLng `json:"northeast"`
}

Viewport is the recommended map viewport for a geocode candidate.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL