How the Atlas World Map Addon Tracks Visited Countries in TREK

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 and accessed through 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 exposes endpoints that delegate to the service layer:

// 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:

// 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, the marking functions execute idempotent SQL operations:

// 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 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:

// 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:

// 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.

The statistics endpoint returns the complete visited list:

// 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.
  • 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 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.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →