<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Itay Shmool</title>
    <description>The latest articles on Forem by Itay Shmool (@itayshmool).</description>
    <link>https://forem.com/itayshmool</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3821052%2F7e861617-82ae-4eb5-81f6-c715d6b2eb3f.png</url>
      <title>Forem: Itay Shmool</title>
      <link>https://forem.com/itayshmool</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/itayshmool"/>
    <language>en</language>
    <item>
      <title>🚀 Just published a deep dive on how I built BuzzOff, an offline-first speed camera alert system!

 If you're into #Python, #FastAPI, #Flutter, or #geospatial data, this one's for you. Learn about the unique data pipeline, SQLite
 R-tree magic for offlin</title>
      <dc:creator>Itay Shmool</dc:creator>
      <pubDate>Thu, 12 Mar 2026 21:14:38 +0000</pubDate>
      <link>https://forem.com/itayshmool/just-published-a-deep-dive-on-how-i-built-buzzoff-an-offline-first-speed-camera-alert-5316</link>
      <guid>https://forem.com/itayshmool/just-published-a-deep-dive-on-how-i-built-buzzoff-an-offline-first-speed-camera-alert-5316</guid>
      <description>&lt;div class="ltag__link"&gt;
  &lt;a href="/itayshmool" class="ltag__link__link"&gt;
    &lt;div class="ltag__link__pic"&gt;
      &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3821052%2F7e861617-82ae-4eb5-81f6-c715d6b2eb3f.png" alt="itayshmool"&gt;
    &lt;/div&gt;
  &lt;/a&gt;
  &lt;a href="https://dev.to/itayshmool/how-i-built-a-python-powered-offline-first-geolocation-alert-system-15d" class="ltag__link__link"&gt;
    &lt;div class="ltag__link__content"&gt;
      &lt;h2&gt;How I Built a Python-Powered, Offline-First Geolocation Alert System&lt;/h2&gt;
      &lt;h3&gt;Itay Shmool ・ Mar 12&lt;/h3&gt;
      &lt;div class="ltag__link__taglist"&gt;
        &lt;span class="ltag__link__tag"&gt;#flutter&lt;/span&gt;
        &lt;span class="ltag__link__tag"&gt;#mobile&lt;/span&gt;
        &lt;span class="ltag__link__tag"&gt;#python&lt;/span&gt;
        &lt;span class="ltag__link__tag"&gt;#showdev&lt;/span&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/a&gt;
&lt;/div&gt;


</description>
    </item>
    <item>
      <title>How I Built a Python-Powered, Offline-First Geolocation Alert System</title>
      <dc:creator>Itay Shmool</dc:creator>
      <pubDate>Thu, 12 Mar 2026 21:12:24 +0000</pubDate>
      <link>https://forem.com/itayshmool/how-i-built-a-python-powered-offline-first-geolocation-alert-system-15d</link>
      <guid>https://forem.com/itayshmool/how-i-built-a-python-powered-offline-first-geolocation-alert-system-15d</guid>
      <description>&lt;p&gt;Like many projects, this one started with a problem. Mine was a speeding ticket. My developer brain immediately jumped from&lt;br&gt;
  frustration to system design: "What would it take to build a better alert system? One that's silent, private, and works without a&lt;br&gt;
  constant internet connection?"&lt;/p&gt;

&lt;p&gt;This post is the technical deep dive into the system I built: BuzzOff. We'll cover the architectural decisions, the Python data&lt;br&gt;
  pipeline, the offline-first "Country Pack" system using SQLite and R-trees, and the core logic of the Flutter app.&lt;/p&gt;

&lt;p&gt;Let's get into the code.&lt;/p&gt;

&lt;p&gt;** Core Architectural Decisions**&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Offline-First is Non-Negotiable: A car is a Faraday cage on wheels. Mobile data is unreliable. An app that relies on a constant
  network connection to check for nearby cameras is doomed to fail. This meant the core logic and data had to live on the device.&lt;/li&gt;
&lt;li&gt;Do the Heavy Lifting on the Backend: While the app must work offline, the user's phone is not the place to be processing
  gigabytes of raw, messy data from dozens of sources. This led to a backend-heavy architecture where a powerful data pipeline does
  all the hard work upfront.&lt;/li&gt;
&lt;li&gt;The Right Tools for the Job:

&lt;ul&gt;
&lt;li&gt;Backend: Python with FastAPI for its incredible async performance, dependency injection system, and automatic documentation
 via Pydantic.&lt;/li&gt;
&lt;li&gt;Database: PostgreSQL with the PostGIS extension for its powerful geospatial querying capabilities during data processing.&lt;/li&gt;
&lt;li&gt;Mobile App: Flutter to build a single, beautiful, and performant app for both iOS and Android from one codebase.&lt;/li&gt;
&lt;li&gt;Admin Panel: React to build a quick and functional internal dashboard for managing the data pipeline.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;**  Deep Dive: The Data Pipeline&lt;br&gt;
**&lt;br&gt;
  The heart of the entire platform is the data pipeline. Its job is to ingest chaotic data from the real world and forge it into clean,&lt;br&gt;
  efficient, and distributable "packs".&lt;/p&gt;

&lt;p&gt;**  1. Ingestion: The Adapter System&lt;br&gt;
**&lt;br&gt;
  I designed a pluggable "adapter" system in Python to fetch data from various sources. The first and most important is for&lt;br&gt;
  OpenStreetMap (OSM). Using the Overpass API, I can pull down camera data for an entire country with a single query.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;    &lt;span class="err"&gt;`&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="c1"&gt;# A simplified look at fetching data from OSM
&lt;/span&gt;    &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;httpx&lt;/span&gt;
    &lt;span class="mi"&gt;3&lt;/span&gt;
    &lt;span class="mi"&gt;4&lt;/span&gt; &lt;span class="c1"&gt;# This query finds all nodes tagged as "speed_camera" within Israel's borders
&lt;/span&gt;    &lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="n"&gt;OVERPASS_QUERY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
    6 [out:json];
    7 area[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ISO3166-1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;IL&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;][admin_level=2];
    8 (node[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;highway&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;speed_camera&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;](area););
    9 out body;
   10 &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
   &lt;span class="mi"&gt;11&lt;/span&gt;
   &lt;span class="mi"&gt;12&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;fetch_osm_cameras&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
   &lt;span class="mi"&gt;13&lt;/span&gt;     &lt;span class="c1"&gt;# Make a POST request to the Overpass API
&lt;/span&gt;   &lt;span class="mi"&gt;14&lt;/span&gt;     &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;httpx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
   &lt;span class="mi"&gt;15&lt;/span&gt;         &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://overpass-api.de/api/interpreter&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
   &lt;span class="mi"&gt;16&lt;/span&gt;         &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;data&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;OVERPASS_QUERY&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
   &lt;span class="mi"&gt;17&lt;/span&gt;     &lt;span class="p"&gt;)&lt;/span&gt;
   &lt;span class="mi"&gt;18&lt;/span&gt;     &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raise_for_status&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
   &lt;span class="mi"&gt;19&lt;/span&gt;     &lt;span class="c1"&gt;# The camera data is in the "elements" key
&lt;/span&gt;   &lt;span class="mi"&gt;20&lt;/span&gt;     &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;elements&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
   &lt;span class="mi"&gt;21&lt;/span&gt;
   &lt;span class="mi"&gt;22&lt;/span&gt; &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Found &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;fetch_osm_cameras&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; cameras in OSM!&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="err"&gt;`&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;This raw data is often messy—duplicates, misplaced tags, etc. It gets dumped into a "raw_cameras" table in our PostgreSQL database&lt;br&gt;
  for the next stage.&lt;/p&gt;

&lt;p&gt;**  2. Processing &amp;amp; Deduplication&lt;br&gt;
**&lt;br&gt;
  Once the data is in PostgreSQL, I use the power of PostGIS to clean it up. The most critical step is deduplication. If one source&lt;br&gt;
  says a camera is at (lat, lon) and another says it's 15 meters away, they are likely the same camera. I run a geospatial query to&lt;br&gt;
  find and merge any cameras within a certain threshold (e.g., 50 meters) of each other.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The "Country Pack" Generator&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is the most crucial part of the architecture. How do you get the data onto the phone for offline use?&lt;/p&gt;

&lt;p&gt;I generate a SQLite file for each country. This isn't just a simple table; it's a highly optimized, portable database. The real magic&lt;br&gt;
  is using an R-tree spatial index.&lt;/p&gt;

&lt;p&gt;An R-tree is a special database index for geospatial data. Instead of indexing a simple value, it indexes a 2D area (a "bounding&lt;br&gt;
  box"). This allows for incredibly fast "what's near me?" queries.&lt;/p&gt;

&lt;p&gt;Here's the Python code that generates a pack:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="c1"&gt;# A conceptual look at the pack generator
&lt;/span&gt;    &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sqlite3&lt;/span&gt;
    &lt;span class="mi"&gt;3&lt;/span&gt;
    &lt;span class="mi"&gt;4&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;generate_pack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;country_code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cameras&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="mi"&gt;5&lt;/span&gt;     &lt;span class="n"&gt;db_file&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;packs/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;country_code&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;.db&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="mi"&gt;6&lt;/span&gt;     &lt;span class="n"&gt;conn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sqlite3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;db_file&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="mi"&gt;7&lt;/span&gt;     &lt;span class="n"&gt;cursor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="mi"&gt;8&lt;/span&gt;
    &lt;span class="mi"&gt;9&lt;/span&gt;     &lt;span class="c1"&gt;# 1. Create the main camera data table
&lt;/span&gt;   &lt;span class="mi"&gt;10&lt;/span&gt;     &lt;span class="n"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
   11         CREATE TABLE cameras (
   12             id TEXT PRIMARY KEY,
   13             lat REAL NOT NULL,
   14             lon REAL NOT NULL,
   15             type TEXT
   16         )
   17     &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
   &lt;span class="mi"&gt;18&lt;/span&gt;
   &lt;span class="mi"&gt;19&lt;/span&gt;     &lt;span class="c1"&gt;# 2. Create the R-tree virtual table. This is the magic.
&lt;/span&gt;   &lt;span class="mi"&gt;20&lt;/span&gt;     &lt;span class="c1"&gt;# It stores the bounding box for each camera.
&lt;/span&gt;   &lt;span class="mi"&gt;21&lt;/span&gt;     &lt;span class="n"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
   22         CREATE VIRTUAL TABLE cameras_rtree USING rtree(
   23             id, min_lat, max_lat, min_lon, max_lon
   24         )
   25     &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
   &lt;span class="mi"&gt;26&lt;/span&gt;
   &lt;span class="mi"&gt;27&lt;/span&gt;     &lt;span class="c1"&gt;# 3. Insert camera data into both tables
&lt;/span&gt;   &lt;span class="mi"&gt;28&lt;/span&gt;     &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;cam&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;cameras&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
   &lt;span class="mi"&gt;29&lt;/span&gt;         &lt;span class="n"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
   &lt;span class="mi"&gt;30&lt;/span&gt;             &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;INSERT INTO cameras VALUES (?, ?, ?, ?)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
   &lt;span class="mi"&gt;31&lt;/span&gt;             &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cam&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;cam&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;lat&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;cam&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;lon&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;cam&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
   &lt;span class="mi"&gt;32&lt;/span&gt;         &lt;span class="p"&gt;)&lt;/span&gt;
   &lt;span class="mi"&gt;33&lt;/span&gt;         &lt;span class="c1"&gt;# For an R-tree, the min/max lat/lon for a point are just the same value
&lt;/span&gt;   &lt;span class="mi"&gt;34&lt;/span&gt;         &lt;span class="n"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
   &lt;span class="mi"&gt;35&lt;/span&gt;             &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;INSERT INTO cameras_rtree VALUES (?, ?, ?, ?, ?)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
   &lt;span class="mi"&gt;36&lt;/span&gt;             &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cam&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;cam&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;lat&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;cam&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;lat&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;cam&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;lon&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;cam&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;lon&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
   &lt;span class="mi"&gt;37&lt;/span&gt;         &lt;span class="p"&gt;)&lt;/span&gt;
   &lt;span class="mi"&gt;38&lt;/span&gt;
   &lt;span class="mi"&gt;39&lt;/span&gt;     &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;commit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
   &lt;span class="mi"&gt;40&lt;/span&gt;     &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
   &lt;span class="mi"&gt;41&lt;/span&gt;     &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Generated &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;db_file&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; successfully!&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="n"&gt;The&lt;/span&gt; &lt;span class="n"&gt;resulting&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt; &lt;span class="nb"&gt;file&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Country Pack.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="n"&gt;The&lt;/span&gt; &lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="n"&gt;downloads&lt;/span&gt; &lt;span class="n"&gt;this&lt;/span&gt; &lt;span class="n"&gt;single&lt;/span&gt; &lt;span class="nb"&gt;file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;giving&lt;/span&gt; &lt;span class="n"&gt;it&lt;/span&gt; &lt;span class="nb"&gt;all&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="n"&gt;it&lt;/span&gt; &lt;span class="n"&gt;needs&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt; &lt;span class="n"&gt;run&lt;/span&gt; &lt;span class="n"&gt;completely&lt;/span&gt;
  &lt;span class="n"&gt;offline&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;**  Deep Dive: The FastAPI Server&lt;br&gt;
**&lt;br&gt;
  The FastAPI backend serves the generated packs and provides an API for the admin panel. Its use of Pydantic for data validation is&lt;br&gt;
  fantastic. You define your data shape once and get validation and serialization for free.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="c1"&gt;# From backend/app/schemas/developer.py
&lt;/span&gt;    &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pydantic&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;BaseModel&lt;/span&gt;
    &lt;span class="mi"&gt;3&lt;/span&gt;
    &lt;span class="mi"&gt;4&lt;/span&gt; &lt;span class="c1"&gt;# This Pydantic model defines the structure for a camera submission
&lt;/span&gt;    &lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="c1"&gt;# FastAPI will automatically validate incoming requests against this
&lt;/span&gt;    &lt;span class="mi"&gt;6&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Camera&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BaseModel&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="mi"&gt;7&lt;/span&gt;     &lt;span class="n"&gt;lat&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;
    &lt;span class="mi"&gt;8&lt;/span&gt;     &lt;span class="n"&gt;lon&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;
    &lt;span class="mi"&gt;9&lt;/span&gt;     &lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fixed_speed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
   &lt;span class="mi"&gt;10&lt;/span&gt;     &lt;span class="n"&gt;speed_limit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
   &lt;span class="mi"&gt;11&lt;/span&gt;     &lt;span class="n"&gt;address&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
  &lt;span class="n"&gt;An&lt;/span&gt; &lt;span class="n"&gt;endpoint&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt; &lt;span class="n"&gt;get&lt;/span&gt; &lt;span class="n"&gt;pack&lt;/span&gt; &lt;span class="n"&gt;metadata&lt;/span&gt; &lt;span class="n"&gt;looks&lt;/span&gt; &lt;span class="n"&gt;beautifully&lt;/span&gt; &lt;span class="n"&gt;simple&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;

    &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="c1"&gt;# From backend/app/api/routes/public.py
&lt;/span&gt;    &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="nd"&gt;@router.get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/packs/{country_code}/meta&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="mi"&gt;3&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_pack_meta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;country_code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="mi"&gt;4&lt;/span&gt;     &lt;span class="c1"&gt;# Logic to find the latest pack file for the country
&lt;/span&gt;    &lt;span class="mi"&gt;5&lt;/span&gt;     &lt;span class="n"&gt;pack&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;find_latest_pack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;country_code&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="mi"&gt;6&lt;/span&gt;     &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;pack&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="mi"&gt;7&lt;/span&gt;         &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;HTTPException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;404&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;detail&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Pack not found&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="mi"&gt;8&lt;/span&gt;     &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="mi"&gt;9&lt;/span&gt;         &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;version&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;pack&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;version&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
   &lt;span class="mi"&gt;10&lt;/span&gt;         &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;camera_count&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;pack&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;camera_count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
   &lt;span class="mi"&gt;11&lt;/span&gt;         &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;file_size_bytes&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;pack&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;file_size_bytes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
   &lt;span class="mi"&gt;12&lt;/span&gt;         &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;checksum_sha256&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;pack&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;checksum_sha256&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
   &lt;span class="mi"&gt;13&lt;/span&gt;     &lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;**  Deep Dive: The Flutter App's Proximity Engine&lt;br&gt;
**&lt;br&gt;
  The Flutter app's job is to use the downloaded SQLite pack to check for nearby cameras. Here’s the core logic that runs every few&lt;br&gt;
  seconds while driving:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Get the phone's current GPS location (lat, lon).&lt;/li&gt;
&lt;li&gt;Calculate a search "bounding box" (e.g., 2km north/south/east/west) around the current location.&lt;/li&gt;
&lt;li&gt;Execute a query against the local SQLite database's R-tree index:&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;1&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;cameras_rtree&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;min_lat&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;max_lat&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;min_lon&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;max_lon&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  This query is incredibly fast, even with tens of thousands of cameras in the pack.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;ol&gt;
&lt;li&gt;For the handful of cameras returned by the R-tree query, run a more precise Haversine distance calculation to get the exact
  distance in meters.&lt;/li&gt;
&lt;li&gt;If any camera is within the alert radius (e.g., 500 meters), check the user's GPS heading. If they are moving towards the camera,
  trigger a haptic feedback (vibration).&lt;/li&gt;
&lt;li&gt;Debounce the alert for that camera ID for a set period (e.g., 2 minutes) to prevent constant vibrations.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This multi-stage filtering process—a broad query with the R-tree followed by a precise calculation on a small subset—is the key to&lt;br&gt;
  making the app both fast and battery-efficient.&lt;/p&gt;

&lt;p&gt;**  Lessons Learned&lt;br&gt;
**&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Pre-calculation is powerful: Doing the heavy data processing ahead of time on a server is a classic but effective pattern for
 mobile apps.&lt;/li&gt;
&lt;li&gt;SQLite is a beast: Don't underestimate SQLite. For read-heavy applications on the edge, it's an incredible tool, especially with
 extensions like R-tree.&lt;/li&gt;
&lt;li&gt;The right tool for each job: Combining the strengths of Python for data, Flutter for UI, and PostGIS for geospatial processing was
 a winning formula.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The project continues to evolve. I've since built a Python SDK to allow community contributions, and I'm working on automating the&lt;br&gt;
  entire data pipeline. What started as a simple problem became a fascinating journey into full-stack application development.&lt;/p&gt;

&lt;p&gt;I hope this deep dive was useful. You can find more about the project at buzzoff.me. I'd love to hear your feedback on the&lt;br&gt;
  architecture in the comments below&lt;/p&gt;

&lt;p&gt;

&lt;/p&gt;
&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
        &lt;div class="c-embed__cover"&gt;
          &lt;a href="https://buzzoff.me/" class="c-link align-middle" rel="noopener noreferrer"&gt;
            &lt;img alt="" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbuzzoff.me%2Fog-image.png" height="auto" class="m-0"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="c-embed__body"&gt;
        &lt;h2 class="fs-xl lh-tight"&gt;
          &lt;a href="https://buzzoff.me/" rel="noopener noreferrer" class="c-link"&gt;
            BuzzOff — Never Get Flashed Again
          &lt;/a&gt;
        &lt;/h2&gt;
          &lt;p class="truncate-at-3"&gt;
            Speed camera alerts that ride with you. Silent. Offline. Always watching the road ahead.
          &lt;/p&gt;
        &lt;div class="color-secondary fs-s flex items-center"&gt;
            &lt;img alt="favicon" class="c-embed__favicon m-0 mr-2 radius-0" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbuzzoff.me%2Fbuzzoff_icon.svg"&gt;
          buzzoff.me
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;





</description>
      <category>flutter</category>
      <category>mobile</category>
      <category>python</category>
      <category>showdev</category>
    </item>
  </channel>
</rss>
