A Rust-based service for storing and querying cell tower location data. The service automatically syncs cell tower data from OpenCellID and provides a REST API for querying cell information.
- Automatic Data Sync: Periodically downloads and updates cell tower data from OpenCellID
- REST API: Query individual cells or fetch multiple cells with filtering and pagination
- Geofence Filtering: Filter cells by geographic bounding box
- Network Filtering: Filter by MCC (Mobile Country Code) and MNC (Mobile Network Code)
- Radio Type Filtering: Filter by radio technology (GSM, UMTS, CDMA, LTE, NR)
- Cursor-based Pagination: Efficiently paginate through large result sets
The service automatically synchronizes cell tower data from OpenCellID.
- Check interval: Every 10 minutes
- Update window: After 4:00 AM UTC (OpenCellID publishes new data at ~3:00 AM UTC)
- Update types:
- Full update: Downloads the complete dataset (~2GB compressed). Triggered on first run, after gaps of more than 24 hours, or at month/year boundaries.
- Diff update: Downloads only changes from the previous day (~few MB). Used for daily incremental updates when the last update was within 24 hours.
- The service checks for updates every 10 minutes
- Before 4:00 AM UTC, updates are skipped to wait for OpenCellID's daily data refresh
- After 4:00 AM UTC, the service determines the update type based on the last successful update:
- Same day: No update needed
- Yesterday (within 24h): Download today's diff file
- Older: Download full dataset
- Rust 1.92.0+
- MySQL/MariaDB database
- Docker (optional, for containerized deployment)
| Variable | Description | Example |
|---|---|---|
DATABASE_URL |
MySQL connection string | mysql://user:pass@localhost/cells |
RUST_LOG |
Log level | info |
OPENCELLID_API_KEY |
API key for OpenCellDD downloads | your-api-key |
CORS_ORIGINS |
Comma-separated list of allowed CORS origins (if not set, all origins are allowed) | https://example.com,https://other.com |
-
Clone the repository
git clone https://github.com/racemap/cell-service.git cd cell-service -
Set up environment
cp .env.example .env # Edit .env with your database credentials and API key -
Set up the database
curl --proto '=https' --tlsv1.2 -LsSf https://github.com/diesel-rs/diesel/releases/latest/download/diesel_cli-installer.sh | sh diesel setup
-
Run the service
cargo run
docker build -t cell-service .
docker run -e DATABASE_URL="mysql://user:pass@host/db" -e OPENCELLID_API_KEY="key" -p 3000:3000 cell-serviceThe service runs on port 3000 by default.
Check if the service is running.
GET /health
Response: OK
Retrieve a specific cell tower by its identifiers.
GET /cell?mcc=<mcc>&net=<mnc>&area=<lac>&cell=<cid>[&radio=<radio>]
Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
mcc |
integer | Yes | Mobile Country Code |
net |
integer | Yes | Mobile Network Code |
area |
integer | Yes | Location Area Code |
cell |
integer | Yes | Cell ID |
radio |
string | No | Radio type: GSM, UMTS, CDMA, LTE, NR |
Example:
curl "http://localhost:3000/cell?mcc=262&net=1&area=12345&cell=67890"Response:
{
"radio": "LTE",
"mcc": 262,
"net": 1,
"area": 12345,
"cell": 67890,
"unit": 1,
"lon": 13.405,
"lat": 52.52,
"cellRange": 1000,
"samples": 50,
"changeable": true,
"created": "2024-01-15T10:30:00Z",
"updated": "2025-12-20T14:00:00Z",
"averageSignal": -85
}Returns null if no cell is found.
Retrieve multiple cells with optional filtering and cursor-based pagination.
GET /cells?[mcc=<mcc>][&mnc=<mnc>][&min_lat=<lat>][&max_lat=<lat>][&min_lon=<lon>][&max_lon=<lon>][&radio=<radio>][&cursor=<cursor>][&limit=<limit>]
Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
mcc |
integer | No | Filter by Mobile Country Code |
mnc |
integer | No | Filter by Mobile Network Code |
min_lat |
float | No | Minimum latitude (geofence) |
max_lat |
float | No | Maximum latitude (geofence) |
min_lon |
float | No | Minimum longitude (geofence) |
max_lon |
float | No | Maximum longitude (geofence) |
radio |
string | No | Filter by radio type: GSM, UMTS, CDMA, LTE, NR |
cursor |
string | No | Pagination cursor from previous response |
limit |
integer | No | Results per page (default: 100, max: 1000) |
Example - Get all cells in Germany (MCC 262):
curl "http://localhost:3000/cells?mcc=262&limit=100"Example - Get LTE cells in Berlin area:
curl "http://localhost:3000/cells?mcc=262&min_lat=52.3&max_lat=52.7&min_lon=13.1&max_lon=13.8&radio=LTE&limit=50"Response:
{
"cells": [
{
"radio": "LTE",
"mcc": 262,
"net": 1,
"area": 12345,
"cell": 67890,
"unit": 1,
"lon": 13.405,
"lat": 52.52,
"cellRange": 1000,
"samples": 50,
"changeable": true,
"created": "2024-01-15T10:30:00Z",
"updated": "2025-12-20T14:00:00Z",
"averageSignal": -85
}
],
"nextCursor": "TFRFOjI2MjoxOjEyMzQ1OjY3ODkw",
"hasMore": true
}Pagination:
To fetch the next page, include the nextCursor value from the previous response along with the same filter parameters:
# First request
curl "http://localhost:3000/cells?mcc=262&min_lat=52.0&max_lat=53.0&limit=100"
# Next page (use same filters + cursor)
curl "http://localhost:3000/cells?mcc=262&min_lat=52.0&max_lat=53.0&limit=100&cursor=TFRFOjI2MjoxOjEyMzQ1OjY3ODkw"When hasMore is false, there are no more results.
Lookup multiple cells by (mcc, mnc, lac, cid) in a single request.
This endpoint returns one best match per input key, aligned 1:1 with the request order.
POST /cells/lookup
Request Body:
{
"cells": [
{"mcc": 262, "mnc": 1, "lac": 12345, "cid": 67890},
{"mcc": 262, "mnc": 1, "lac": 124, "cid": 457}
]
}Notes / Constraints:
- Max keys per request: 50. If more are sent, the response is padded with
nullfor the excess entries. - If multiple rows exist for the same
(mcc, mnc, lac, cid)(e.g. different radios), the service picks a single deterministic “best” row:- Higher
samples - Newer
updated - Higher radio generation (
NR>LTE>UMTS>GSM>CDMA)
- Higher
Example:
curl -X POST "http://localhost:3000/cells/lookup" \
-H "Content-Type: application/json" \
-d '{
"cells": [
{"mcc": 262, "mnc": 1, "lac": 12345, "cid": 67890},
{"mcc": 999, "mnc": 999, "lac": 999, "cid": 999}
]
}'Response:
{
"cells": [
{
"radio": "LTE",
"mcc": 262,
"net": 1,
"area": 12345,
"cell": 67890,
"unit": 1,
"lon": 13.405,
"lat": 52.52,
"cellRange": 1000,
"samples": 50,
"changeable": true,
"created": "2024-01-15T10:30:00Z",
"updated": "2025-12-20T14:00:00Z",
"averageSignal": -85
},
null
]
}# Unit tests
cargo test
# Integration tests (requires Docker)
cargo test --features integration_testsSee LICENSE for details.