The hard part is the data
Every municipality publishes its schedule differently. Many publish a scan or a phone photo of a printed calendar: twelve months on one page, colour-coded days and a legend in the corner.
The obvious approach is to give a model the whole page and ask for the dates. On six hard scans the best version of that prompt got 24.5% of the cells wrong. A page holds hundreds of similar numbers, and where the model can't see each day clearly it fills in a regular pattern. In Wiśniew bio waste goes out once a month. The whole-page prompt returned it every week.
How the pipeline reads it
- Cut the page into monthsThe model marks the month frames, code moves the borders into the empty space between them, and a crop is rejected if its last visible day is wrong.
- Read each piece twiceTwo prompts read every month, with the legend attached. Tables get a second image with the columns spread apart.
- Break ties by majorityWhen the two reads disagree, a third read decides.
- Let code check what is certainSundays, public holidays, gaps in the rhythm, row counts, and that every address in the national register falls into exactly one zone.
- Hold back instead of guessingDoubt that the reads can't settle holds the zone back. A missing schedule is safer than a wrong date.
Results
Six scans and photos with 2,190 cells. The pipeline read each of them in three full runs, 6,570 cell results in all. A cell is one place, one waste type and one month, and it only counts as right if every day matches the document.
Green is right, red is wrong, grey is held back. The ground truth is 966 decisions I made by hand in the admin panel. The pipeline was tuned on these documents, so zero wrong dates is its best case, on documents it already knows. Documents it has not seen are the next test.
| Document | Cells | Prompt wrong | Pipeline wrong | Held back |
|---|---|---|---|---|
| Gierałtów, photo | 120 | 57.2% | 0 | 0% |
| Czerna, photo | 112 | 86.9% | 0 | 0% |
| Wykroty, photo | 120 | 76.7% | 0 | 0% |
| Wiśniew, scan | 147 | 77.8% | 0 | 33.3% |
| Szprotawa, sideways scan | 264 | 1.0% | 0 | 74.2% |
| Klimontów, three-page scan of tables | 1,427 | 11.3% | 0 | 10.1% |
| All six | 2,190 | 24.5% | 0 | 17.8% |
It is not better everywhere: in Szprotawa the prompt was nearly right and the pipeline held three quarters back. It also costs more: one call and about 25 seconds per document for the prompt, against a median of 33 calls and 4.5 minutes.
What I built
- The iPhone app in SwiftUI, with widgets, a Live Activity, reminders you can snooze or mark as done, and a schedule that works offline.
- An Android app in Kotlin and Jetpack Compose. It is in closed testing on Google Play.
- The backend in FastAPI and PostgreSQL: address search against the national TERYT register, schedules served as static JSON.
- Two-factor login for the admin panel: TOTP written from RFC 6238, one-time backup codes and 40 tests.
- The reading pipeline in Python with OpenCV, Tesseract, PyMuPDF and Gemini, and an admin panel in React and TypeScript.
- Sortuś, an assistant that answers questions using the resident's own schedule.
Six municipalities are published in the app. EcoSort on the App Store. The website is ecosort.pl.





