towerops/lib/towerops/geocoding.ex
2026-02-14 13:57:25 -06:00

103 lines
3 KiB
Elixir

defmodule Towerops.Geocoding do
@moduledoc """
Service for geocoding addresses using Google Maps Geocoding API.
Supports both system-wide API keys and organization-specific encrypted API keys.
"""
require Logger
@google_geocoding_url "https://maps.googleapis.com/maps/api/geocode/json"
@doc """
Geocode an address to latitude/longitude coordinates.
## Parameters
- `address`: The address string to geocode
## Returns
- `{:ok, %{latitude: float, longitude: float, formatted_address: string}}` on success
- `{:error, reason}` on failure
"""
def geocode(address) when is_binary(address) do
with {:ok, api_key} <- get_api_key(),
{:ok, response} <- make_geocoding_request(address, api_key) do
parse_geocoding_response(response)
end
end
defp get_api_key do
api_key = get_system_api_key()
if api_key && String.trim(api_key) != "" do
{:ok, String.trim(api_key)}
else
{:error, :no_api_key}
end
end
defp get_system_api_key do
Application.get_env(:towerops, :google_maps_api_key) ||
System.get_env("GOOGLE_MAPS_API_KEY")
end
defp make_geocoding_request(address, api_key) do
params = %{
"address" => address,
"key" => api_key
}
case Req.get(@google_geocoding_url, params: params, timeout: 10_000) do
{:ok, %Req.Response{status: 200, body: body}} ->
{:ok, body}
{:ok, %Req.Response{status: status_code, body: body}} ->
Logger.warning("Geocoding API returned status #{status_code}: #{inspect(body)}")
{:error, "Geocoding API error (status #{status_code})"}
{:error, reason} ->
Logger.error("Failed to make geocoding request: #{inspect(reason)}")
{:error, "Network error: #{inspect(reason)}"}
end
end
defp parse_geocoding_response(%{"status" => "OK", "results" => [result | _]}) do
with %{"geometry" => %{"location" => %{"lat" => lat, "lng" => lng}}} <- result,
%{"formatted_address" => formatted_address} <- result do
{:ok,
%{
latitude: lat,
longitude: lng,
formatted_address: formatted_address
}}
else
_ -> {:error, "Invalid response format from Google Maps API"}
end
end
defp parse_geocoding_response(%{"status" => "ZERO_RESULTS"}) do
{:error, "No results found for the given address"}
end
defp parse_geocoding_response(%{"status" => "OVER_QUERY_LIMIT"}) do
{:error, "API quota exceeded. Please check your Google Maps API usage."}
end
defp parse_geocoding_response(%{"status" => "REQUEST_DENIED", "error_message" => message}) do
{:error, "API request denied: #{message}"}
end
defp parse_geocoding_response(%{"status" => "INVALID_REQUEST"}) do
{:error, "Invalid request. Please check the address format."}
end
defp parse_geocoding_response(%{"status" => status}) do
{:error, "Geocoding failed with status: #{status}"}
end
defp parse_geocoding_response(_response) do
{:error, "Unexpected response format from Google Maps API"}
end
end