# How the Atlas World Map Addon Tracks Visited Countries in TREK

> Discover how the Atlas world map addon tracks visited countries in TREK. It uses manual inputs and automatic trip data with geocoding and caching for accurate country code resolution.

- Repository: [Maurice/TREK](https://github.com/mauriceboe/TREK)
- Tags: internals
- Published: 2026-07-04

---

**The Atlas world map addon tracks visited countries through a hybrid approach that combines manual user markings stored in SQLite tables with automatic derivation from trip place data, utilizing background geocoding and multi-layer caching to resolve location coordinates to country codes.**

The Atlas addon in the TREK repository visualizes an interactive world map that highlights every country or sub-national region a user has visited. According to the TREK source code, this tracking system employs both explicit manual markings and automatic resolution from place coordinates attached to user trips, storing results in a normalized SQLite schema and exposing aggregated data via REST endpoints.

## Data Storage Architecture for Country Tracking

The tracking system relies on three core tables defined in [`shared/src/atlas/atlas.schema.ts`](https://github.com/mauriceboe/TREK/blob/main/shared/src/atlas/atlas.schema.ts) and accessed through [`src/db/database.ts`](https://github.com/mauriceboe/TREK/blob/main/src/db/database.ts).

- **`visited_countries`** – Stores manual user markings with columns `user_id`, `country_code`, and `created_at`.
- **`visited_regions`** – Optional sub-national granularity with `user_id`, `country_code`, `region_code`, `region_name`, and `created_at`.
- **`place_regions`** – Caches reverse-geocode results for trip places, linking `place_id` to resolved `country_code` and `region_code`.

This schema separates persistent user intent (manual marks) from derived trip data (automatic resolution), allowing the system to merge both sources at query time.

## Manual Marking Flow

Users can explicitly add or remove countries via the Atlas UI, which triggers a synchronized client-server flow.

### API Endpoints for Marking Countries

The NestJS controller in [`src/nest/atlas/atlas.controller.ts`](https://github.com/mauriceboe/TREK/blob/main/src/nest/atlas/atlas.controller.ts) exposes endpoints that delegate to the service layer:

```typescript
// src/nest/atlas/atlas.controller.ts
@Patch(':code')
async markVisited(@Param('code') code: string, @UserId() userId: number) {
  this.atlasService.markCountryVisited(userId, code.toUpperCase());
}

```

The client initiates these requests through [`client/src/pages/atlas/atlasModel.ts`](https://github.com/mauriceboe/TREK/blob/main/client/src/pages/atlas/atlasModel.ts):

```typescript
// client/src/pages/atlas/atlasModel.ts
await fetch(`/api/atlas/visited-countries/${countryCode}`, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
});

```

### Database Operations

In [`src/services/atlasService.ts`](https://github.com/mauriceboe/TREK/blob/main/src/services/atlasService.ts), the marking functions execute idempotent SQL operations:

```typescript
// src/services/atlasService.ts
export function markCountryVisited(userId: number, code: string): void {
  db.prepare('INSERT OR IGNORE INTO visited_countries (user_id, country_code) VALUES (?, ?)').run(userId, code);
}
export function unmarkCountryVisited(userId: number, code: string): void {
  db.prepare('DELETE FROM visited_countries WHERE user_id = ? AND country_code = ?').run(userId, code);
}

```

The `INSERT OR IGNORE` pattern prevents duplicate entries, while the `DELETE` statement removes explicit markings without affecting automatically derived trip data.

## Automatic Derivation from Trip Places

When generating statistics, Atlas automatically resolves every place attached to a user’s trips into country codes, then aggregates the results.

### Resolving Places to Country Codes

The `resolvePlaceCountries` function in [`src/services/atlasService.ts`](https://github.com/mauriceboe/TREK/blob/main/src/services/atlasService.ts) processes each place through `resolveCountryCodeSync`, which attempts resolution in this order:

1. **`getCountryFromAddress`** – Parses the address string for a country code.
2. **`getCountryFromCoords`** – Performs a bounding-box pre-filter (`COUNTRY_BOXES`) followed by an exact polygon test for lat/long coordinates.
3. **`reverseGeocodeCountry`** – Enqueues a background Nominatim API request if cache misses occur, storing results in `place_regions`.

### Aggregation and Merging Logic

The `getStats` function aggregates resolved places into a `countrySet` Map, then merges manual entries:

```typescript
// src/services/atlasService.ts – aggregation core
export async function getStats(userId: number) {
  const trips = getUserTrips(userId);
  const places = getPlacesForTrips(trips.map(t => t.id));

  const placeCountries = resolvePlaceCountries(places);
  const countrySet = new Map<string, CountryEntry>();
  for (const place of places) {
    const code = placeCountries.get(place.id);
    if (code) {
      if (!countrySet.has(code)) countrySet.set(code, { code, places: [], tripIds: new Set() });
      const entry = countrySet.get(code)!;
      entry.places.push({ id: place.id, name: place.name, lat: place.lat ?? null, lng: place.lng ?? null });
      entry.tripIds.add(place.trip_id);
    }
  }
  // Merge manual marks
  const manual = db.prepare('SELECT country_code FROM visited_countries WHERE user_id = ?')
                     .all(userId) as { country_code: string }[];
  for (const mc of manual) {
    if (!countrySet.has(mc.country_code)) {
      countrySet.set(mc.country_code, { code: mc.country_code, places: [], tripIds: new Set() });
    }
  }
  // ...
}

```

This ensures that manually marked countries appear in the final dataset even when no geocoded places exist for them.

## Background Geocoding and Caching Strategy

To minimize external API calls, Atlas implements a multi-layer caching system.

### The Geo Bundle and Spatial Lookups

On startup, the service loads a pre-built gzipped GeoJSON bundle (`loadGeoBundle`) containing admin-0 (countries) and admin-1 (regions) geometries. The `COUNTRY_BOXES` constant provides fast bounding-box pre-filtering before expensive polygon inclusion tests.

### Persistent Caching Layers

Three cache layers prevent redundant geocoding:

- **`geocodeCache`** – In-memory Map for recent lat-lng to country lookups.
- **`regionCache`** – In-memory Map for lat-lng to region lookups.
- **`place_regions` table** – Persistent SQLite storage for resolved place coordinates.

When synchronous resolution fails, the system enqueues background requests:

```typescript
// background task in resolvePlaceCountries()
if (p.lat && p.lng && !geocodingInFlight.has(p.id)) {
  uncachedForGeocode.push(p);
}

```

The background worker updates `place_regions` upon completion and clears the in-flight tracking set.

## Client-Side API Consumption

The client consumes aggregated data through two primary endpoints defined in [`src/nest/atlas/atlas.controller.ts`](https://github.com/mauriceboe/TREK/blob/main/src/nest/atlas/atlas.controller.ts).

The statistics endpoint returns the complete visited list:

```typescript
// client/src/pages/atlas/atlasModel.ts
const resp = await fetch('/api/atlas/stats');
const { countries } = await resp.json();
// Each item: {code, placeCount, tripCount, manually_marked?, ...}

```

The `manually_marked` boolean flag indicates entries sourced from `visited_countries` rather than automatic derivation. For country-specific details, the client calls `GET /atlas/:code`, which invokes `getCountryPlaces` to list all associated places and trips.

## Summary

- **Hybrid tracking** combines explicit manual markings in `visited_countries` with automatic resolution from trip place coordinates.
- **Manual control** allows users to add or remove countries via `markCountryVisited` and `unmarkCountryVisited` in [`src/services/atlasService.ts`](https://github.com/mauriceboe/TREK/blob/main/src/services/atlasService.ts).
- **Automatic derivation** uses `resolveCountryCodeSync` with address parsing, bounding-box filters, and polygon tests to map places to country codes.
- **Triple caching** via `geocodeCache`, `regionCache`, and the `place_regions` table eliminates redundant Nominatim API calls.
- **REST endpoints** in [`atlas.controller.ts`](https://github.com/mauriceboe/TREK/blob/main/atlas.controller.ts) serve merged data to the Leaflet map interface, distinguishing manual marks with a `manually_marked` flag.

## Frequently Asked Questions

### How does Atlas handle countries when place coordinates are missing?

When coordinates are absent or resolution fails, the system relies solely on the `visited_countries` table for that user. If a user has manually marked the country, it appears in the statistics; otherwise, it is omitted until geocoding completes or the user adds the mark manually.

### Can users mark sub-national regions as visited?

Yes, the schema supports region-level tracking through the `visited_regions` table, which stores `region_code` and `region_name` alongside `country_code`. The same manual marking flow applies, allowing the Atlas map to highlight specific states or provinces.

### What happens if a user manually marks a country that also has trip places?

The aggregation logic in `getStats` merges both sources. The country appears in the result set with `manually_marked: true`, and the associated places from trips are appended to the entry’s places array. This prevents duplicates while preserving the distinction between automatic and manual tracking.

### How does the system avoid hitting Nominatim API rate limits?

Atlas employs a three-tier caching strategy: in-memory Maps for recent lookups, a persistent `place_regions` table for resolved coordinates, and a background queue that processes only uncached places asynchronously. Places currently being geocoded are tracked in `geocodingInFlight` to prevent duplicate requests.