Something went wrong. Try again.
Imports from your Foursquare data export into Anchor atproto records.
Something went wrong. Try again.
fsq2anchor geocoder.go
4.0 kB · 144 lines
Go
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145package main
import ( "encoding/json" "fmt" "io" "net/http" "net/url" "time")
// LocationIQGeocoder handles reverse geocoding via LocationIQtype LocationIQGeocoder struct { apiToken string httpClient *http.Client lastCall time.Time}
// LocationIQResponse is the API response structuretype LocationIQResponse struct { PlaceID string `json:"place_id"` DisplayName string `json:"display_name"` Lat string `json:"lat"` Lon string `json:"lon"` Address LocationIQAddress `json:"address"`}
// LocationIQAddress contains structured address componentstype LocationIQAddress struct { HouseNumber string `json:"house_number,omitempty"` Road string `json:"road,omitempty"` Suburb string `json:"suburb,omitempty"` City string `json:"city,omitempty"` Town string `json:"town,omitempty"` Village string `json:"village,omitempty"` County string `json:"county,omitempty"` State string `json:"state,omitempty"` Postcode string `json:"postcode,omitempty"` Country string `json:"country,omitempty"` CountryCode string `json:"country_code,omitempty"`}
// NewLocationIQGeocoder creates a new geocoder with the given API tokenfunc NewLocationIQGeocoder(token string) *LocationIQGeocoder { return &LocationIQGeocoder{ apiToken: token, httpClient: &http.Client{ Timeout: 10 * time.Second, }, }}
// Reverse performs reverse geocoding (lat/lng to address)func (g *LocationIQGeocoder) Reverse(lat, lng float64) (*LocationIQResponse, error) { // Rate limit: LocationIQ free tier is 2 requests/second elapsed := time.Since(g.lastCall) if elapsed < 500*time.Millisecond { time.Sleep(500*time.Millisecond - elapsed) } g.lastCall = time.Now()
u, _ := url.Parse("https://us1.locationiq.com/v1/reverse") q := u.Query() q.Set("key", g.apiToken) q.Set("lat", fmt.Sprintf("%f", lat)) q.Set("lon", fmt.Sprintf("%f", lng)) q.Set("format", "json") q.Set("addressdetails", "1") u.RawQuery = q.Encode()
resp, err := g.httpClient.Get(u.String()) if err != nil { return nil, fmt.Errorf("geocode request failed: %w", err) } defer resp.Body.Close()
if resp.StatusCode == 429 { return nil, fmt.Errorf("rate limit exceeded") }
if resp.StatusCode != http.StatusOK { bodyBytes, _ := io.ReadAll(resp.Body) return nil, fmt.Errorf("geocode failed (status %d): %s", resp.StatusCode, string(bodyBytes)) }
var result LocationIQResponse if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { return nil, fmt.Errorf("failed to parse geocode response: %w", err) }
return &result, nil}
// ExtractAddress converts LocationIQ response to our AddressObjectfunc ExtractAddress(resp *LocationIQResponse, venueName string) AddressObject { addr := resp.Address
// Build street address street := addr.Road if addr.HouseNumber != "" && street != "" { street = street + " " + addr.HouseNumber }
// Get locality (city, town, or village) locality := addr.City if locality == "" { locality = addr.Town } if locality == "" { locality = addr.Village }
// Get country code (uppercase) country := addr.CountryCode if country != "" { country = fmt.Sprintf("%s", country) // Keep as-is, it's already lowercase } else { country = "XX" // Unknown }
return AddressObject{ Name: venueName, Street: street, Locality: locality, Region: addr.State, PostalCode: addr.Postcode, Country: country, }}
// PrepareCheckinParamsWithGeocode builds checkin params with geocoded addressfunc PrepareCheckinParamsWithGeocode(checkin ParsedCheckin, geocoder *LocationIQGeocoder) CreateCheckinParams { params := PrepareCheckinParams(checkin, "")
if geocoder != nil { resp, err := geocoder.Reverse(checkin.Lat, checkin.Lng) if err == nil { params.Address = ExtractAddress(resp, checkin.Venue.Name) } // On error, keep the default address with just venue name }
return params}