Help us shape the future of Go! Take the 2026 Go Developer Survey

thailocate

package module
v0.2.0 Latest Latest
Warning

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

Go to latest
Published: Aug 7, 2026 License: MIT Imports: 5 Imported by: 0

README

thailocate

Resolve a WGS84 latitude/longitude to the Thai administrative area that contains it — province (จังหวัด), district (อำเภอ, or เขต inside Bangkok), and subdistrict (ตำบล, or แขวง inside Bangkok).

  • Boundary data is embedded in the binary (go:embed) — no network calls, no database, no external API, at runtime.
  • Pure standard library — no third-party Go dependencies.
  • Covers all 77 provinces / 928 districts / 7,425 subdistricts of Thailand.
  • Typical lookup: well under 1ms.
  • Usable two ways: as an imported library function, or via a small HTTP server you run yourself so other services can call it over a URL.

Boundary polygons are derived from the UN OCHA COD-AB administrative boundaries for Thailand (Royal Thai Survey Department data, via HDX), simplified for size. Because it's a static snapshot, double check district/subdistrict names against an official source before using this for anything legally sensitive (boundaries do occasionally get redrawn).

1. Use it as a library

go get github.com/siwakorne/thailocate
package main

import (
	"fmt"
	"github.com/siwakorne/thailocate"
)

func main() {
	// one-off, uses a shared lazily-initialized instance
	detail, err := thailocate.GetLocationDetail(13.7563, 100.5018)
	if err != nil {
		panic(err)
	}
	fmt.Println(detail.ProvinceEN, detail.DistrictEN, detail.SubdistrictEN)
	// Bangkok Phra Nakhon Bowon Niwet
}

If you're making many calls (e.g. from inside an HTTP handler), construct one Locator at startup and reuse it — it's safe for concurrent use:

loc, err := thailocate.New()
if err != nil {
	log.Fatal(err)
}

detail, _ := loc.GetLocationDetail(18.7883, 98.9853)
Response shape
type LocationDetail struct {
	Latitude, Longitude float64
	Found                bool

	CountryEN, CountryTH string

	ProvinceEN, ProvinceTH, ProvinceCode string

	DistrictEN, DistrictTH, DistrictCode string
	DistrictType string // "amphoe" or "khet" (Bangkok)

	SubdistrictEN, SubdistrictTH, SubdistrictCode string
	SubdistrictType string // "tambon" or "khwaeng" (Bangkok)

	MatchLevel string // "subdistrict" | "district" | "province" | "none"
}

Found = false / MatchLevel = "none" means the point isn't in Thailand — that's a normal result, not an error. An error is only returned for malformed input (e.g. lat/lng out of range).

Polygon → many subdistricts

GetLocationDetail answers "which one subdistrict contains this point?". FindSubdistrictsIntersecting answers the inverse, many-to-many question: "which subdistricts does this polygon touch or overlap?" — useful for things like a delivery/shift zone that spans several tambon/khwaeng.

matches, err := loc.FindSubdistrictsIntersecting(thailocate.Geometry{
	Type: "Polygon",
	Coordinates: [][][2]float64{{ // GeoJSON ring: [lng, lat] pairs
		{100.490, 13.720},
		{100.540, 13.720},
		{100.540, 13.755},
		{100.490, 13.755},
		{100.490, 13.720},
	}},
})
if err != nil {
	log.Fatal(err)
}
for _, m := range matches {
	fmt.Println(m.DistrictEN, m.SubdistrictEN)
}

Coordinates also accepts "MultiPolygon" ([][][][2]float64), and either form also accepts the generic []any nesting you get back from decoding arbitrary JSON/BSON into any (e.g. straight out of a MongoDB driver decode) — no need to re-type already-decoded geometry.

The result is a []SubdistrictMatch (same province/district/subdistrict fields as LocationDetail, minus the point-specific ones) and may be empty — not an error — if the polygon falls entirely outside Thailand. An error is only returned for malformed input (bad Type, empty or non-numeric coordinates). Intersection is tested against each subdistrict's outer boundary only; holes are not subtracted (subdistrict polygons in this dataset rarely have any).

2. Use it over a URL

A small HTTP server ships in cmd/thailocate-server:

go run ./cmd/thailocate-server
# boundary data loaded, starting server on :8080

curl "http://localhost:8080/v1/locate?lat=13.7563&lng=100.5018"
{
  "latitude": 13.7563,
  "longitude": 100.5018,
  "found": true,
  "country_en": "Thailand",
  "country_th": "ประเทศไทย",
  "province_en": "Bangkok",
  "province_th": "กรุงเทพมหานคร",
  "province_code": "TH10",
  "district_en": "Phra Nakhon",
  "district_th": "พระนคร",
  "district_code": "TH1001",
  "district_type": "khet",
  "subdistrict_en": "Bowon Niwet",
  "subdistrict_th": "บวรนิเวศ",
  "subdistrict_code": "TH100107",
  "subdistrict_type": "khwaeng",
  "match_level": "subdistrict"
}

The same polygon → many-subdistricts query is available as POST /v1/subdistricts-intersecting, body is a raw GeoJSON geometry object:

curl -X POST http://localhost:8080/v1/subdistricts-intersecting \
  -d '{
        "type": "Polygon",
        "coordinates": [[[100.490,13.720],[100.540,13.720],[100.540,13.755],[100.490,13.755],[100.490,13.720]]]
      }'
[
  {
    "province_en": "Bangkok", "province_th": "กรุงเทพมหานคร", "province_code": "TH10",
    "district_en": "Bang Rak", "district_th": "บางรัก", "district_code": "TH1004", "district_type": "khet",
    "subdistrict_en": "Si Lom", "subdistrict_th": "สีลม", "subdistrict_code": "TH100402", "subdistrict_type": "khwaeng"
  },
  { "...": "one entry per intersecting subdistrict" }
]

"type" may also be "MultiPolygon". An empty [] response means the polygon doesn't intersect any subdistrict (not an error); a 400 means the geometry itself was malformed.

Change the listen address with -addr, e.g. go run ./cmd/thailocate-server -addr :9090. Build a standalone binary with go build -o thailocate-server ./cmd/thailocate-server — it's fully self-contained (data is embedded), so you can just copy the one binary to a server and run it.

How it works

  1. Boundary polygons (province / district / subdistrict, data/*.geojson) are compiled into the binary with go:embed and parsed once at startup.
  2. A lookup does a bounding-box pre-check followed by an even-odd ray-casting point-in-polygon test, starting at the subdistrict level and falling back to district, then province, if a point falls in a small gap between simplified polygons (rare — mostly right on a coastline).
  3. Everything runs in memory; there's no per-request I/O.

Project layout

thailocate/
├── go.mod
├── locator.go            # public API: Locator, LocationDetail, New(), GetLocationDetail(),
│                         # SubdistrictMatch, FindSubdistrictsIntersecting()
├── geometry.go            # point-in-polygon, polygon-intersects-polygon + GeoJSON parsing (stdlib only)
├── data/                  # embedded boundary polygons
│   ├── province.geojson
│   ├── amphoe.geojson
│   └── tambon.geojson
├── cmd/thailocate-server/ # optional HTTP server
└── example/basic/         # minimal usage example

Publishing your own fork

This repo (github.com/siwakorne/thailocate) is already published this way — tagged and go get-able. If you fork it under a different module path, here's how to publish your copy:

  1. Create a public repo on GitHub (or GitLab/etc.).
  2. Update the module path in go.mod to match that repo exactly, and update the two import "github.com/siwakorne/..." lines in cmd/thailocate-server/main.go and example/basic/main.go to match too.
  3. Commit everything (code and the data/*.geojson files — they need to ship with the module so go:embed has something to embed) and push.
  4. Tag a version, following semver:
    git tag v0.1.0
    git push origin v0.1.0
    
  5. That's it. Anyone can now run:
    go get github.com/you/thailocate@v0.1.0
    # or, to just run the server without cloning:
    go install github.com/you/thailocate/cmd/thailocate-server@latest
    
    The first time someone fetches it, Google's public module proxy (proxy.golang.org, the default GOPROXY) pulls it from GitHub, caches it, and serves it from then on — you don't run or pay for anything.

A few things worth doing before/soon after your first tag:

  • Keep the module small enough to be practical. The embedded data is ~6MB; that's fine for go get, just don't be surprised the repo isn't tiny.
  • Add a go.sum: not needed here since there are zero external dependencies, but if you add any later, commit go.sum too so consumers get reproducible, verified builds.
  • Semver matters: once you tag v1.0.0+, any breaking change to the public API (LocationDetail fields, function signatures) needs a v2 module path bump per Go's module rules — so it's worth living on v0.x.y until the API feels settled.
  • Optional polish: add a pkg.go.dev badge (it indexes automatically once the module is fetched at least once) and a short CHANGELOG.md.

License

Code is MIT-licensed — see LICENSE.

The boundary data in data/*.geojson is not covered by that license: it's CC BY-IGO (redistribution and commercial use are fine, attribution is required). See DATA_ATTRIBUTION.md for the required credit line — keep that file alongside the code (and any compiled binaries that embed the data) if you redistribute this.

Notes / limitations

  • If you fork this under your own module path, update go.mod (currently github.com/siwakorne/thailocate) and the import paths in cmd/thailocate-server and example/basic to match — see Publishing your own fork.
  • Polygons are simplified for size (~6MB of embedded data total), so results right on a border are approximate to a few tens of meters at worst.
  • No postal codes included. If you need them, join SubdistrictCode (ADM3_PCODE, e.g. TH100101) against a postal-code dataset such as thailand-geography-data/thailand-geography-json.

Documentation

Overview

Package thailocate resolves a WGS84 latitude/longitude coordinate to the Thai administrative area that contains it: province (จังหวัด), district (อำเภอ, or เขต inside Bangkok) and subdistrict (ตำบล, or แขวง inside Bangkok).

Boundary data is embedded in the compiled binary, so lookups never touch the network or disk at runtime.

Basic usage:

loc, err := thailocate.New()
if err != nil { ... }
detail, err := loc.GetLocationDetail(13.7563, 100.5018)

A ready-to-use default instance is also available without constructing one yourself:

detail, err := thailocate.GetLocationDetail(13.7563, 100.5018)

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Geometry added in v0.2.0

type Geometry struct {
	Type        string `json:"type"`
	Coordinates any    `json:"coordinates"`
}

Geometry is a GeoJSON-style Polygon or MultiPolygon describing an arbitrary area to query with FindSubdistrictsIntersecting — e.g. a rider shift zone, delivery area, or any other caller-defined boundary. It is intentionally not tied to the embedded administrative boundary data.

Coordinates follows standard GeoJSON coordinate nesting for Type: rings of [lng, lat] pairs, with ring[0] the outer boundary and any further rings holes cut out of it.

  • "Polygon": [][][2]float64 (one ring set)
  • "MultiPolygon": [][][][2]float64 (one ring set per disjoint part)

Coordinates also accepts the generic nesting ([]any of []any of ... numbers) produced by decoding arbitrary JSON/BSON into `any`, so callers can pass a value straight out of a database decode without re-typing it into the concrete Go shape above — including unmarshaling a standard GeoJSON geometry object straight off the wire, since json.Unmarshal populates Coordinates with exactly that nesting.

type LocationDetail

type LocationDetail struct {
	Latitude  float64 `json:"latitude"`
	Longitude float64 `json:"longitude"`
	Found     bool    `json:"found"`

	CountryEN string `json:"country_en,omitempty"`
	CountryTH string `json:"country_th,omitempty"`

	ProvinceEN   string `json:"province_en,omitempty"`
	ProvinceTH   string `json:"province_th,omitempty"`
	ProvinceCode string `json:"province_code,omitempty"`

	// District is the อำเภอ (Amphoe) — or เขต (Khet) for the 50 districts
	// of Bangkok. DistrictType tells you which term applies.
	DistrictEN   string `json:"district_en,omitempty"`
	DistrictTH   string `json:"district_th,omitempty"`
	DistrictCode string `json:"district_code,omitempty"`
	DistrictType string `json:"district_type,omitempty"` // "amphoe" | "khet"

	// Subdistrict is the ตำบล (Tambon) — or แขวง (Khwaeng) for Bangkok's
	// subdistricts. SubdistrictType tells you which term applies.
	SubdistrictEN   string `json:"subdistrict_en,omitempty"`
	SubdistrictTH   string `json:"subdistrict_th,omitempty"`
	SubdistrictCode string `json:"subdistrict_code,omitempty"`
	SubdistrictType string `json:"subdistrict_type,omitempty"`

	MatchLevel MatchLevel `json:"match_level"`
}

LocationDetail is everything thailocate knows about a coordinate.

func GetLocationDetail

func GetLocationDetail(lat, lng float64) (*LocationDetail, error)

GetLocationDetail is a package-level convenience wrapper around a lazily initialized, shared Locator. Use this if you just want to call thailocate.GetLocationDetail(lat, lng) without managing a Locator yourself.

type Locator

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

Locator holds the parsed boundary polygons in memory and answers lookups. It is safe for concurrent use by multiple goroutines.

func New

func New() (*Locator, error)

New parses the embedded boundary data and returns a ready-to-use Locator. This does a few megabytes of JSON parsing, so construct one Locator and reuse it (it's safe for concurrent use) rather than calling New per request.

func (*Locator) FindSubdistrictsIntersecting added in v0.2.0

func (l *Locator) FindSubdistrictsIntersecting(geometry Geometry) ([]SubdistrictMatch, error)

FindSubdistrictsIntersecting resolves an arbitrary polygon — e.g. a rider shift zone or delivery area — to every subdistrict (tambon/khwaeng) whose boundary it touches or overlaps. It is the many-to-many counterpart of GetLocationDetail: a single point resolves to at most one subdistrict, but a polygon commonly spans several.

The result is a slice in no particular order, and an empty slice (not an error) is a normal result if the polygon falls entirely outside Thailand or in a gap between simplified boundaries. An error is only returned for a malformed geometry (missing/unsupported Type, empty or non-numeric coordinates).

func (*Locator) GetLocationDetail

func (l *Locator) GetLocationDetail(lat, lng float64) (*LocationDetail, error)

GetLocationDetail resolves lat/lng (WGS84 decimal degrees) to the Thai province / district / subdistrict that contains it.

If the point falls outside Thailand, or in a gap in the simplified boundary data, Found is false and MatchLevel is "none" — this is not treated as an error, since "not in Thailand" is a perfectly valid answer. An error is only returned for malformed input.

type MatchLevel

type MatchLevel string

MatchLevel indicates the finest administrative level that was actually resolved for a query. Lookups almost always resolve at the subdistrict level; province/district-only results are a rare fallback for points that fall in tiny gaps between simplified polygons (e.g. right at a coastline).

const (
	MatchSubdistrict MatchLevel = "subdistrict"
	MatchDistrict    MatchLevel = "district"
	MatchProvince    MatchLevel = "province"
	MatchNone        MatchLevel = "none"
)

type SubdistrictMatch added in v0.2.0

type SubdistrictMatch struct {
	ProvinceEN   string `json:"province_en"`
	ProvinceTH   string `json:"province_th"`
	ProvinceCode string `json:"province_code"`

	// District is the อำเภอ (Amphoe) — or เขต (Khet) for the 50 districts
	// of Bangkok. DistrictType tells you which term applies.
	DistrictEN   string `json:"district_en"`
	DistrictTH   string `json:"district_th"`
	DistrictCode string `json:"district_code"`
	DistrictType string `json:"district_type"` // "amphoe" | "khet"

	// Subdistrict is the ตำบล (Tambon) — or แขวง (Khwaeng) for Bangkok's
	// subdistricts. SubdistrictType tells you which term applies.
	SubdistrictEN   string `json:"subdistrict_en"`
	SubdistrictTH   string `json:"subdistrict_th"`
	SubdistrictCode string `json:"subdistrict_code"`
	SubdistrictType string `json:"subdistrict_type"`
}

SubdistrictMatch is one administrative area (province/district/subdistrict) returned by FindSubdistrictsIntersecting.

func FindSubdistrictsIntersecting added in v0.2.0

func FindSubdistrictsIntersecting(geometry Geometry) ([]SubdistrictMatch, error)

FindSubdistrictsIntersecting is a package-level convenience wrapper around a lazily initialized, shared Locator. See Locator.FindSubdistrictsIntersecting.

Directories

Path Synopsis
cmd
thailocate-server command
Command thailocate-server exposes the thailocate library over HTTP.
Command thailocate-server exposes the thailocate library over HTTP.
example
basic command

Jump to

Keyboard shortcuts

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