> ## Documentation Index
> Fetch the complete documentation index at: https://docs.lokpanchang.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Reverse geocoding

> How coordinates are turned into timezone and city: OpenCage API, longitude-based fallback, and CityDetail caching

The application needs to turn a user’s **latitude and longitude** into a **timezone** (for panchangam and 6:30 AM scheduling) and into **city/state/country** (for display and caching). This is done by **reverse geocoding**: coordinates → place name and timezone. The codebase uses **OpenCage** as the primary source and a **longitude-based fallback** when OpenCage is unavailable or returns no result. Results are cached in the **`CityDetail`** table to reduce API calls and keep user records linked to a canonical place.

## Overview

* **Primary:** [OpenCage Geocoding API](https://opencagedata.com/) — reverse geocode (lat, lon) to get timezone, city, state, country, and optional address/confidence.
* **Fallback:** Longitude-based timezone estimation and simple location labels (no external API), in **`helpers/simple_timezone_fallback.py`**.
* **Caching:** **`get_city_hybrid`** (and thus **`get_or_create_city`**) returns or creates a **`CityDetail`** row so the same (city, state, country) is reused and the user’s `city_id` points to it.

Required environment variable: **`OPENCAGE_API_KEY`**. Without it, the primary path raises when the geocoder is first used; the fallback is only used after an OpenCage call fails (e.g. network error or empty result).

## Main functions

### `get_timezone_hybrid(lat, lon)`

**File:** `helpers/hybrid_geocoding.py`

Returns the **IANA timezone name** (e.g. `America/New_York`, `Asia/Kolkata`) for the given coordinates.

1. **OpenCage** — Call `reverse_geocode(lat, lon)`, read `annotations.timezone.name` from the first result. If present, return it and log.
2. **Fallback** — If OpenCage fails or returns no timezone, call **`get_timezone_from_longitude(lat, lon)`** in `simple_timezone_fallback.py`, which maps longitude ranges to a fixed set of timezone names. If that fails, return `"UTC"`.

Used when the app only needs a timezone (e.g. to decide scheduling or to pass into panchangam). The webhook location handler uses it first, then may overwrite with the city’s timezone when **`get_city_hybrid`** is used.

### `get_location_info_hybrid(lat, lon)`

**File:** `helpers/hybrid_geocoding.py`

Returns a **dictionary** with city, state, country, timezone, and metadata (no database write).

1. **OpenCage** — Reverse geocode and parse `components` (city/town/village/municipality/suburb/county/state) and `annotations.timezone`. City is resolved with a long fallback chain (city → town → village → … → county → state → `"Unknown Location"`). Result includes `source: 'opencage'`, confidence, formatted address.
2. **Fallback** — If OpenCage fails, call **`get_simple_location_info(lat, lon)`** in `simple_timezone_fallback.py`, which uses **`get_timezone_from_longitude`** and coarse lat/lon rules (e.g. North America, India) to produce a minimal dict with city/state/country/timezone.

Used internally by **`get_city_hybrid`**; not typically called directly from the main app.

### `get_city_hybrid(lat, lon)`

**File:** `helpers/hybrid_geocoding.py`

Returns a **`CityDetail`** instance (existing or newly created) for the given coordinates.

1. Call **`get_location_info_hybrid(lat, lon)`** to get city, state, country, timezone.
2. Look up **`CityDetail`** by `(city, state, country)`. If found, optionally update its `timezone` if it was missing or different, then return it.
3. If not found, create a new **`CityDetail`** with city, state, country, timezone, latitude, longitude; commit and return.

This is the function that **caches** reverse-geocoding results in the database. The main app and admin portal use it when they need a `CityDetail` to attach to a user (e.g. `user.city_id`) and for display names in panchangam.

### `get_or_create_city(lat, lon)`

**File:** `helpers/city_utils.py`

Thin wrapper that delegates to **`get_city_hybrid(lat, lon)`**. Exists for backward compatibility and clearer naming at call sites. Same behavior: returns a **`CityDetail`** or `None` if geocoding fails.

### `get_timezone_from_opencage(lat, lon)`

**File:** `helpers/city_utils.py`

Alias for **`get_timezone_hybrid`** (imported from `hybrid_geocoding`). Used by tests and any code that expects a “from OpenCage” name; behavior is the same hybrid (OpenCage first, then longitude fallback).

## Longitude-based fallback

**File:** `helpers/simple_timezone_fallback.py`

* **`get_timezone_from_longitude(lat, lon)`** — Splits the globe into longitude bands and returns a fixed IANA timezone per band (e.g. -90 to -67.5 → `America/New_York`, 60–75 → `Asia/Kolkata`). Latitude is not used. Intended as a last resort when APIs fail.
* **`get_simple_location_info(lat, lon)`** — Returns a dict with `city`, `state`, `country`, `timezone` using the same timezone logic plus coarse regional rules (e.g. North America, India) to set generic labels. Used by **`get_location_info_hybrid`** when OpenCage fails.

This fallback avoids external calls but is approximate (no real place names beyond broad region, and no DST or political-boundary nuance).

## Caching in CityDetail

The **`CityDetail`** model (see [Database](/database)) stores:

* `city`, `state`, `country` — Used as the unique logical key together (lookup in `get_city_hybrid`).
* `timezone` — Can be updated when an existing row is found but had no timezone or a different one.
* `latitude`, `longitude` — Stored on create; not used for lookup.

By using **`get_city_hybrid`** (or **`get_or_create_city`**) whenever the app needs a place for coordinates:

* Repeated coordinates (or same city/state/country from OpenCage) reuse one row.
* **UserDetail** rows reference it via **`city_id`**, so panchangam and admin can show a single canonical name and timezone per user.

## Where it’s used in the codebase

* **`app.py`** — **Location webhook:** `get_timezone_hybrid(lat, lon)` for initial timezone; **`get_city_hybrid(lat, lon)`** for `CityDetail`, then user’s `timezone` is set from the city’s timezone if available, and `user.city_id` is set. **Test route** (e.g. generate-by-coords): **`get_city_hybrid`** for optional city display.
* **`admin_portal/app.py`** — When an admin updates a user’s location (new lat/lon), **`get_timezone_hybrid`** and **`get_city_hybrid`** are used to resolve timezone and city and update the user record.
* **Tests** — e.g. `test_app_integration.py` uses **`get_timezone_from_opencage`** and **`get_or_create_city`** to verify geocoding and DB caching.

## Summary

| Goal                  | Function                                 | Primary source                       | Fallback                                    |
| --------------------- | ---------------------------------------- | ------------------------------------ | ------------------------------------------- |
| Timezone only         | `get_timezone_hybrid`                    | OpenCage `annotations.timezone.name` | `get_timezone_from_longitude` → else `UTC`  |
| Location dict (no DB) | `get_location_info_hybrid`               | OpenCage components + timezone       | `get_simple_location_info`                  |
| City row (cached)     | `get_city_hybrid` / `get_or_create_city` | Above + CityDetail lookup/insert     | Same as above; if both fail, returns `None` |

All of this lives under **`helpers/`** (hybrid\_geocoding.py, city\_utils.py, simple\_timezone\_fallback.py). The **`opencage`** Python package is used to call the OpenCage API; the key is configured via **`OPENCAGE_API_KEY`**.
