package places import ( "context" "encoding/json" "fmt" "math" "net/http" "net/url" "strings" "time" ) // Place — бар/ресторан/кафе в радиусе type Place struct { OSMID int64 `json:"osm_id"` Name string `json:"name"` Type string `json:"type"` Cuisine string `json:"cuisine,omitempty"` Lat float64 `json:"lat"` Lng float64 `json:"lng"` Distance float64 `json:"distance_m"` Address string `json:"address,omitempty"` Phone string `json:"phone,omitempty"` Website string `json:"website,omitempty"` Hours string `json:"hours,omitempty"` } type Client struct { httpClient *http.Client endpoint string } func New() *Client { return &Client{ httpClient: &http.Client{Timeout: 8 * time.Second}, endpoint: "https://overpass-api.de/api/interpreter", } } // NearbyPlaces — бары/рестораны/кафе рядом (Overpass API) func (c *Client) NearbyPlaces(ctx context.Context, lat, lng, radiusKm float64, limit int) ([]Place, error) { if limit <= 0 || limit > 100 { limit = 30 } // bounding box latDelta := radiusKm / 111.0 lngDelta := radiusKm / (111.0 * math.Cos(lat*math.Pi/180)) south := lat - latDelta north := lat + latDelta west := lng - lngDelta east := lng + lngDelta query := fmt.Sprintf(`[out:json][timeout:8]; ( node["amenity"~"bar|pub|restaurant|cafe|biergarten|nightclub"](%f,%f,%f,%f); way["amenity"~"bar|pub|restaurant|cafe|biergarten|nightclub"](%f,%f,%f,%f); ); out center %d;`, south, west, north, east, south, west, north, east, limit) data := url.Values{} data.Set("data", query) req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.endpoint, strings.NewReader(data.Encode())) if err != nil { return nil, err } req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("User-Agent", "BuhApp/0.7 (https://buhapp.mygoodservice.ru)") resp, err := c.httpClient.Do(req) if err != nil { return nil, err } defer resp.Body.Close() if resp.StatusCode != 200 { return nil, fmt.Errorf("overpass status %d", resp.StatusCode) } var raw struct { Elements []struct { Type string `json:"type"` ID int64 `json:"id"` Lat float64 `json:"lat"` Lon float64 `json:"lon"` Center *struct { Lat float64 `json:"lat"` Lon float64 `json:"lon"` } `json:"center"` Tags map[string]string `json:"tags"` } `json:"elements"` } if err := json.NewDecoder(resp.Body).Decode(&raw); err != nil { return nil, err } out := make([]Place, 0, len(raw.Elements)) for _, e := range raw.Elements { plat, plng := e.Lat, e.Lon if e.Center != nil { plat, plng = e.Center.Lat, e.Center.Lon } if plat == 0 && plng == 0 { continue } name := e.Tags["name"] if name == "" { name = e.Tags["name:ru"] } if name == "" { continue } // расстояние от точки юзера dist := haversine(lat, lng, plat, plng) * 1000 if dist > radiusKm*1000 { continue } out = append(out, Place{ OSMID: e.ID, Name: name, Type: e.Tags["amenity"], Cuisine: e.Tags["cuisine"], Lat: plat, Lng: plng, Distance: dist, Address: e.Tags["addr:full"], Phone: e.Tags["phone"], Website: e.Tags["website"], Hours: e.Tags["opening_hours"], }) } return out, nil } func haversine(lat1, lng1, lat2, lng2 float64) float64 { const R = 6371.0 dLat := (lat2 - lat1) * math.Pi / 180 dLng := (lng2 - lng1) * math.Pi / 180 a := math.Sin(dLat/2)*math.Sin(dLat/2) + math.Cos(lat1*math.Pi/180)*math.Cos(lat2*math.Pi/180)*math.Sin(dLng/2)*math.Sin(dLng/2) c := 2 * math.Atan2(math.Sqrt(a), math.Sqrt(1-a)) return R * c }