I am a 39yo developer in Cuba. I built a ride-hailing app from scratch — not by coding everything from zero, but by taking Organic Maps, stripping it down to its bones, generating my own maps from OpenStreetMap data, and adding a lightweight backend.
The app completed over 20,000 real taxi rides during a fuel crisis, with phones that have 1GB RAM and intermittent 2G signals.
This is the technical story of how I did it.
The Core Architecture (Before We Dive In)
Before explaining the challenges, here is what I built:
- Passenger app (58MB): Requests rides, searches addresses offline, tracks nearby drivers on the map.
- Driver app (62MB): Receives requests, accepts rides, shares real-time position.
- Backend (lightweight Python/Django): Matches passengers with nearest drivers using real road distance (OSRM), no Nominatim.
-
Custom map pipeline (running on my machine): Downloads OpenStreetMap data, cleans it, removes all unnamed polygons and irrelevant languages, builds a super-fast search index directly into the
.mwmfile.
Everything works offline except the initial ride match. The passenger can browse the map, search for addresses, and save favorite spots with no internet connection at all.
Challenge 1: Shrinking the Map from 160MB to 60MB
Organic Maps is a beautiful, full-featured navigation app. But it was never designed to be a taxi dispatch system. The original app weighed over 160MB — fine for personal navigation, but a disaster for budget Android phones in Cuba.
The problem: I needed two separate apps (passenger and driver) that share the same map core. Neither needs hiking trails, cycling routes, or world map coverage for places nobody visits.
But here is the real constraint: the map data itself is the biggest part of the app.
What I Did (Beyond Stripping Code)
Most people think "removing features from the app code" is the answer. I did that (removed track recording, multi-threaded rendering, cloud bookmark sync). But the real size reduction came from generating my own map files.
Organic Maps uses .mwm files (Map With Me format). The standard ones include:
- All languages (from Arabic to Vietnamese)
- Every polygon from OpenStreetMap — including unnamed fields, forests with no label, empty lots
- Full metadata for every point of interest
I built a custom map generation pipeline:
- Download OpenStreetMap data for Cuba.
- Filter out every polygon that has no name tag — no more anonymous fields, empty parking lots, unnamed buildings. The taxi app doesn't need them.
- Remove all languages except Spanish and English. A typical
.mwmfile stores translations for 50+ languages. By keeping only two, I cut the metadata section by roughly 80%. - During generation, I built a custom search index and embedded it directly into the
.mwmfile. This means:- The app does not need Nominatim (an online geocoding service)
- Search is instantaneous and offline
- The index is compressed and lives alongside the map geometry
- The output is a clean
.mwmfile with only what a taxi app needs: named streets, named places, and a fast search index.
The result: My custom Cuba map is 25MB. The official Organic Maps Cuba map is over 70MB.
What I Did With the Organic Maps Codebase
Once I had small map files, I could also remove code bloat:
| What I removed | Why |
|---|---|
| Track recording | Riders don't need to save their route history |
| Multi-threaded renderer | Saves RAM; drivers don't need 60fps panning |
| Cloud bookmark sync | No cloud in Cuba; sync only locally |
| World map tiles | Only Cuba is needed |
What I kept and repurposed:
-
BookmarkManagerbecame the "Favorite Places" feature. Passengers save home, work, etc., and request a ride with one tap. The storage engine is local, fast, and already there.
The final APK sizes:
- Passenger app: 58MB
- Driver app: 62MB
Boot time dropped from 8 seconds to under 3 seconds on old Android phones.
Challenge 2: Offline Search Without Nominatim
In a normal app, when you type "Calle 23 esq. L, Vedado", the app sends your query to Nominatim (a geocoding service that runs on OpenStreetMap data). Nominatim responds with coordinates. This requires an internet connection and a server.
I cannot afford a server in Cuba. I cannot rely on the passenger having internet when they need to search for an address.
The solution: I built the search index into the map file itself during generation.
How the Custom Search Index Works
Organic Maps has a powerful offline search system: SearchEngine, Geocoder, Processor, and Ranker . But it expects the .mwm file to have standard metadata. I replaced that metadata with my own index.
My index includes:
- Street names with normalized spelling (accounting for misspellings and abbreviations common in Cuba)
- Intersections (e.g., "23 y L")
- Named places (e.g., "Parque Central", "Capitolio")
- Coordinates for every entry
When the passenger types a query, my app:
- Tokenizes the query locally (no server call)
- Searches the embedded index using a custom fuzzy matching algorithm
- Returns results in under 2 seconds
What I don't need: No Nominatim, no server, no internet connection.
Bonus: Native Markers for Vehicle Tracking (Without Bloating the Backend)
Passengers need to see nearby drivers on the map. Drivers need to see their own position and passenger pickup locations.
In Organic Maps, markers (UserMark, Bookmark, SearchMarkPoint) are native UI elements that sit on top of the map . They are lightweight, built for real-time updates, and do not require redrawing the underlying map tiles.
What I did:
Driver app: Uses a custom
TaxiDriverMark(inherits fromUserMark) to show the driver's own position. Updates every 3 seconds via UDP (discussed in Challenge 3).Passenger app: The backend sends a list of nearby driver positions (coordinates and status). The passenger app renders each as a car icon using
UserMark.UserMarkstores only coordinates, an icon ID, and a unique ID — no geometry, no heavy rendering.Marker reuse: Instead of destroying and recreating markers every time driver positions update, I modify the existing
UserMarkcoordinates. This avoids garbage collection pauses on low-RAM phones.
Why this matters: The MapView class in Organic Maps handles marker rendering efficiently. By using the existing marker system, I got smooth vehicle tracking with almost no additional code. I did not have to build a custom canvas renderer or fight with the OpenGL pipeline.
Challenge 3: Achieving 1MB per Hour Data Usage (The Real Version)
This was the hardest constraint. Not technical — psychological.
In Cuba, mobile data is expensive. If my app consumed 100MB per hour, drivers would uninstall it. They cannot afford to "waste" data on an app that does not directly earn them money.
The target: 1MB per hour of active use. That is roughly 0.3KB per second.
The Simple but Clever Solution
I did not invent new protocols. I did not use UDP or binary formats. I used standard HTTPS requests, but I optimized every single byte.
How it works:
Passenger and driver apps send their position to the server every 10 seconds. That is it. No constant connection, no WebSocket, no streaming.
Each request is a simple HTTPS POST. But the real trick is in the networking layer.
Trick 1: Pre-assigned IP Addresses (No DNS Resolution)
DNS lookup requires a network request before your actual request. That request consumes data and, more importantly, adds latency and points of failure.
In Cuba, DNS servers can be slow or unresponsive. If DNS fails, your API call fails — even if the server is perfectly fine.
What I did: I hardcoded the server's IP address directly into the app during the build. The app never performs a DNS lookup for the primary API endpoint.
The fallback: If the pre-assigned IP address fails (server migration, network change), the app falls back to normal DNS resolution. It caches the new IP address and uses it for subsequent requests. No user intervention required.
Data saved per request: One DNS lookup avoided = approximately 100-200 bytes per session start, plus faster response time.
Trick 2: Everything Happens in the Sync Cycle
Each 10-second position update is not just "here is my location". It is a full synchronization event:
- Upload: Current position, current ride status (idle, searching, matched, in-progress), battery level, any pending actions.
- Download: Available ride requests (for drivers), assignment notifications, ride status changes, driver positions (for passengers).
Why this matters: No separate polling endpoints. No "check for messages" calls. Every request does double duty — reporting and receiving.
Real-world example: A driver accepts a ride. The passenger app, at its next 10-second sync, receives the "driver assigned" notification. The maximum delay is 10 seconds. That is acceptable for a taxi service.
What the Server Does
The server is a simple HTTPS API (Python/Go). For each incoming position update:
- Updates the user's last known position in the database.
- Checks for any pending notifications (new ride request, assignment, cancellation).
- If the user is a passenger and has an active request, the server runs the matching logic (using OSRM for real road distance — see Challenge 4).
- Returns all relevant data in a compact JSON response (no unnecessary fields, no pretty printing).
The Actual Data Usage Breakdown
Let me be precise. Each HTTPS request includes:
- TCP + TLS handshake (amortized over multiple requests if keep-alive is working — but in Cuba, connections drop often)
-
HTTP headers (minimal:
Host,Content-Type,Content-Length, plus a small auth token) - Body (position: ~50 bytes, plus any pending payloads)
The server response is similarly small: a status code, maybe a ride assignment object.
Typical per-request cost: 200-400 bytes up + 200-400 bytes down = 0.5KB to 0.8KB per sync.
Per hour: 6 syncs per minute × 60 minutes = 360 syncs per hour × 0.6KB average = 216KB per hour.
That is before counting the initial connection setup (DNS if fallback is used, TCP handshake, TLS negotiation). Add another 1-2KB for the first request of each session.
Real-world measured usage: Between 0.8MB and 1.2MB per hour, depending on how often the connection resets and how many rides trigger additional payloads.
Why HTTPS and Not Something Fancy?
Because HTTPS works everywhere. It traverses proxies, firewalls, and aggressive telecom throttling. It is reliable. It is debuggable.
I do not need WebSockets. I do not need UDP. I do not need a custom binary protocol. I need reliable, minimal, frequent HTTPS requests — and that is exactly what this architecture delivers.
The One Place I Did Optimize: JSON Payloads
I keep JSON compact:
- No whitespace (single line)
- Short field names (
latinstead oflatitude,rinstead ofrequest_id) - No optional fields if they are empty
- No nested structures unless necessary
Example position update:
{"lat":23.1234,"lon":-82.1234,"r":null,"s":1}
The Real-World Result
- 0.8MB to 1.2MB per hour depending on ride frequency.
- A driver running the app for an 8-hour shift stays under 10MB total.
- Several drivers told me this is the only reason they kept using the app — they could leave it running all day without worrying about their data balance.
What These Challenges Taught Me
1. Data size matters more than code size.
The biggest savings came from generating my own map files, not from stripping code. Removing unnamed polygons and extra languages cut the Cuba map from 200MB to 60MB. That is a 70% reduction.
2. Offline search is possible without Nominatim.
By building a custom search index directly into the .mwm file, I eliminated the need for a geocoding server altogether. The search is fast, offline, and costs nothing to run.
3. Real road distance beats straight lines.
OSRM made the difference between "closest driver as the crow flies" and "closest driver who can actually reach you in reasonable time". Passenger wait times dropped by 40%.
4. Use what the map engine already gives you.
- Native markers (
UserMark) for vehicle tracking saved months of UI work. - The bookmark system (
BookmarkManager) became the "favorite places" feature with no new code. - The rendering pipeline was overkill, but I could remove parts without breaking the core.
5. 1MB per hour is possible if you are ruthless.
Binary protocols, batched updates, UDP, compression, and SMS fallback — every byte had to be justified.
What Comes Next
I have already applied for a grant from NLnet to liberate the core logic (the custom map generator, the embedded search index, and the native marker tracking) as an open-source library.
If funded, I will release:
- The pipeline for generating clean
.mwmfiles from OpenStreetMap extracts - The embedded search index builder
- The lightweight OSRM integration layer for ride matching
How You Can Help
- Star this post if you believe in offline-first infrastructure.
- Share it with anyone building for low-connectivity markets (rural Africa, Asia, Latin America).
- Reach out if you want to test the library in your region.
I am one developer in Cuba. I built something that works. Imagine what we could build together.
P.S. The code is not yet open-source. I am cleaning it up. Follow me for updates when the library drops.
P.P.S. If you work at Organic Maps: thank you. Your map engine was the right foundation. I just had to shrink it, a lot.
This article was originally published by DEV Community and written by Leonardo TQ.
Read original article on DEV Community