# Geo Detection

This page explains how the GeoLite2 databases are enabled, how IP locations are cached, and the conditions for the four geo anomaly checks, including impossible travel.

## Enabling

| Setting | Result |
|---|---|
| Both `Filepath.CityDB` and `Filepath.CountryDB` empty | No GeoLite2 instance; geo detection off |
| Both fail to open | Logs a warning; geo detection off |
| Only `CountryDB` opens | `calcGeo` requires `CityDB`, so geo detection is skipped entirely |
| `CityDB` opens | On; `CountryDB` is only a fallback when the City lookup fails |

Download the database (`GeoLite2-City.mmdb`) from MaxMind yourself; this library does not update it.

## Lookup and History

| Step | Behavior |
|---|---|
| Private IP | Skips the database and returns an empty location (no country or city, coordinates 0) |
| Cache | `geo:ip:{ip}` keeps the lookup result for 24 hours |
| Not found | Logged and skipped; the request's geo score is 0 |
| History | `geo:locations:{sid}` pushes `{ms}:{country code}:{city}:{lat}:{lng}`, keeps the last 10, TTL 24 hours |

## Four Checks

| Flag | Condition | Score |
|---|---|---|
| `geo_high_risk` | History contains a country code listed in `HighRiskCountry` | `ScoreGeoHighRisk` (30) |
| `geo_hopping` | More than 4 distinct countries within the last hour | `ScoreGeoHopping` (15) |
| `geo_frequent_switching` | Within the last hour, `>= 4` cities, `>= 5` records, and more than 4 city switches between adjacent records | `ScoreGeoFrequentSwitch` (20) |
| `rapid_geo_change` | The two newest records are under an hour apart and the implied speed is `> 800` km/h, or the move is `> 500` km within 30 minutes | `ScoreGeoRapidChange` (25) |

`geo_high_risk` currently never fires because of a field mismatch; see [Known Issues](/known-issues).

## Impossible Travel

Distance uses the haversine formula with an Earth radius of 6371 km, and speed is distance divided by the time gap. 800 km/h is roughly airliner cruising speed, which normal movement never exceeds, while VPN node switches and rotating proxy pools do.

| Example | Distance | Gap | Result |
|---|---|---|---|
| Taipei → Tokyo | about 2,100 km | 10 minutes | Fires (both the speed and 30-minute rules hold) |
| Taipei → Taichung | about 140 km | 20 minutes | Does not fire (420 km/h) |
| Taipei → Tokyo | about 2,100 km | 3 hours | Not compared (over one hour) |

## Boundaries

| Case | Behavior |
|---|---|
| One session alternating between private and public IPs | Private records sit at (0, 0), so switching to a public IP looks like a jump of thousands of kilometers from the Atlantic and may trigger `rapid_geo_change` |
| GeoLite2 accuracy | City-level locations can be off by tens of kilometers; different IPs in one city only affect the city-switch count |
| Debug output | `calcGeo` prints the location string through the standard `log.Print` on every request; see [Known Issues](/known-issues) |
