Imports from your Foursquare data export into Anchor atproto records.
at main 144 lines 4.0 kB view raw
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}