Imports from your Foursquare data export into Anchor atproto records.
1package main
2
3import (
4 "encoding/json"
5 "fmt"
6 "io"
7 "net/http"
8 "net/url"
9 "time"
10)
11
12// LocationIQGeocoder handles reverse geocoding via LocationIQ
13type LocationIQGeocoder struct {
14 apiToken string
15 httpClient *http.Client
16 lastCall time.Time
17}
18
19// LocationIQResponse is the API response structure
20type LocationIQResponse struct {
21 PlaceID string `json:"place_id"`
22 DisplayName string `json:"display_name"`
23 Lat string `json:"lat"`
24 Lon string `json:"lon"`
25 Address LocationIQAddress `json:"address"`
26}
27
28// LocationIQAddress contains structured address components
29type LocationIQAddress struct {
30 HouseNumber string `json:"house_number,omitempty"`
31 Road string `json:"road,omitempty"`
32 Suburb string `json:"suburb,omitempty"`
33 City string `json:"city,omitempty"`
34 Town string `json:"town,omitempty"`
35 Village string `json:"village,omitempty"`
36 County string `json:"county,omitempty"`
37 State string `json:"state,omitempty"`
38 Postcode string `json:"postcode,omitempty"`
39 Country string `json:"country,omitempty"`
40 CountryCode string `json:"country_code,omitempty"`
41}
42
43// NewLocationIQGeocoder creates a new geocoder with the given API token
44func NewLocationIQGeocoder(token string) *LocationIQGeocoder {
45 return &LocationIQGeocoder{
46 apiToken: token,
47 httpClient: &http.Client{
48 Timeout: 10 * time.Second,
49 },
50 }
51}
52
53// Reverse performs reverse geocoding (lat/lng to address)
54func (g *LocationIQGeocoder) Reverse(lat, lng float64) (*LocationIQResponse, error) {
55 // Rate limit: LocationIQ free tier is 2 requests/second
56 elapsed := time.Since(g.lastCall)
57 if elapsed < 500*time.Millisecond {
58 time.Sleep(500*time.Millisecond - elapsed)
59 }
60 g.lastCall = time.Now()
61
62 u, _ := url.Parse("https://us1.locationiq.com/v1/reverse")
63 q := u.Query()
64 q.Set("key", g.apiToken)
65 q.Set("lat", fmt.Sprintf("%f", lat))
66 q.Set("lon", fmt.Sprintf("%f", lng))
67 q.Set("format", "json")
68 q.Set("addressdetails", "1")
69 u.RawQuery = q.Encode()
70
71 resp, err := g.httpClient.Get(u.String())
72 if err != nil {
73 return nil, fmt.Errorf("geocode request failed: %w", err)
74 }
75 defer resp.Body.Close()
76
77 if resp.StatusCode == 429 {
78 return nil, fmt.Errorf("rate limit exceeded")
79 }
80
81 if resp.StatusCode != http.StatusOK {
82 bodyBytes, _ := io.ReadAll(resp.Body)
83 return nil, fmt.Errorf("geocode failed (status %d): %s", resp.StatusCode, string(bodyBytes))
84 }
85
86 var result LocationIQResponse
87 if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
88 return nil, fmt.Errorf("failed to parse geocode response: %w", err)
89 }
90
91 return &result, nil
92}
93
94// ExtractAddress converts LocationIQ response to our AddressObject
95func ExtractAddress(resp *LocationIQResponse, venueName string) AddressObject {
96 addr := resp.Address
97
98 // Build street address
99 street := addr.Road
100 if addr.HouseNumber != "" && street != "" {
101 street = street + " " + addr.HouseNumber
102 }
103
104 // Get locality (city, town, or village)
105 locality := addr.City
106 if locality == "" {
107 locality = addr.Town
108 }
109 if locality == "" {
110 locality = addr.Village
111 }
112
113 // Get country code (uppercase)
114 country := addr.CountryCode
115 if country != "" {
116 country = fmt.Sprintf("%s", country) // Keep as-is, it's already lowercase
117 } else {
118 country = "XX" // Unknown
119 }
120
121 return AddressObject{
122 Name: venueName,
123 Street: street,
124 Locality: locality,
125 Region: addr.State,
126 PostalCode: addr.Postcode,
127 Country: country,
128 }
129}
130
131// PrepareCheckinParamsWithGeocode builds checkin params with geocoded address
132func PrepareCheckinParamsWithGeocode(checkin ParsedCheckin, geocoder *LocationIQGeocoder) CreateCheckinParams {
133 params := PrepareCheckinParams(checkin, "")
134
135 if geocoder != nil {
136 resp, err := geocoder.Reverse(checkin.Lat, checkin.Lng)
137 if err == nil {
138 params.Address = ExtractAddress(resp, checkin.Venue.Name)
139 }
140 // On error, keep the default address with just venue name
141 }
142
143 return params
144}