acikpoz
Turkish public construction unit-price (birim fiyat) catalog PDFs into structured pozes
Open source Open in the app JSON README (API)
About
Turkish public construction unit-price (birim fiyat) catalog PDFs into structured pozes
Details
- Kind
- MCP servers
- Topic
- No topic detected
- Publisher
- gulmezeren2-byte
- Origin
- official
- Category
- ferramentas
- Transport
- local
- Version
- 0.2.0
- Stars
- 1
- Last push
- 2026-08-04T06:55:06Z
- Repository state
- ativo
- Language
- Python
- License
- MIT
- Added
- 2026-08-29 03:02:54
- Updated
- 2026-08-29 03:02:54
- Origin id
io.github.gulmezeren2-byte/acikpoz
README
# acikpoz
<!-- mcp-name: io.github.gulmezeren2-byte/acikpoz -->
**Turkish public construction unit prices, turned from a PDF into data you can compute with.**
[](pyproject.toml)
[](LICENSE)
Every public construction job in Turkey is priced against a government catalog: the ÇŞB
(Çevre, Şehircilik ve İklim Değişikliği Bakanlığı) *birim fiyat* books — thousands of unit
prices (a poz: a code, a description, a unit, a price) that estimators, contractors and
auditors all read. They ship as long PDFs. acikpoz turns those pages back into structured
records, deterministically, so a cost estimate or a tender preparation can be *computed*
instead of copied by hand.
It is disciplined geometry, not a language model: it groups the words on a page into visual
rows and reads the **price column's position from the page header** rather than guessing a
fixed spot. That one decision is what makes it trustworthy — some sections (Sıhhi Tesisat,
say) print two numbers per row, the real *Birim Fiyat* and a separate *Montaj Bedeli*, and a
naive parser silently reads the wrong one. acikpoz reads the header, so it reads the right
column.
Paired with [ihalent](https://github.com/gulmezeren2-byte/ihalent) (which structures tender
*results*), acikpoz covers the other half of Turkey's public construction economy: the
*prices* the results are measured against.
## The one rule it never breaks
A price is only ever a number the catalog actually printed in the price column. It never
coerces, never borrows a neighbour's figure, never guesses. Two consequences, both on
purpose:
- A **group-header poz** — a category title like `EVİYELER:` whose sub-items carry the real
prices — has no price of its own. acikpoz leaves its `fiyat` as `None` and flags
`is_group_header`. It does not invent a zero.
- A poz the catalog left without a printed price is a **gap**, surfaced and counted, not
hidden. Every result reports its coverage: how many pozes were priced, how many were
headers, how many were genuine gaps.
Half the value of a cost tool is refusing to make up the numbers the source did not print.
That is the same honesty discipline as [andon](https://github.com/gulmezeren2-byte/andon)
and ihalent.
## Install
```
pip install acikpoz # add [mcp] for the MCP server: pip install "acikpoz[mcp]"
```
## Quick start
Point it at an official catalog PDF you have (acikpoz ships the parser, **not** the data):
```
acikpoz parse bf2026.pdf --pages 8-20
acikpoz parse bf2026.pdf --json > pozes.jsonl # one poz per line, for pipelines
acikpoz parse bf2026.pdf --csv pozes.csv # Excel-ready (utf-8-sig, Turkish text)
acikpoz parse bf2026.pdf --group 25 # only Sıhhi Tesisat pozes
acikpoz parse bf2026.pdf --priced-only # drop headers and gaps
```
The table view — this is a real run against the published 2026 catalog, not a
mock-up:

Two things in that picture are the whole point:
- Three rows carry `-` in the price column. The catalog printed no price for
them, so acikpoz prints none either — they are counted as **gaps**, not filled
in from a neighbour.
- `validate` then flags what the parse is *not sure about*: on this page five
units (`+2000`, `azami`, `Yapı`, `-30°C`, `2,5`) are fragments of description
text that landed in the unit column. The extraction is imperfect on some
layouts, and the tool reports that itself rather than handing you a clean-looking
table with wrong units in it.
Reproduce it — the page is chosen, nothing else is:
```
acikpoz parse bf2026.pdf --pages 400
acikpoz validate bf2026.pdf --pages 400
```
The catalog is the ministry's own file, [published here][catalog]. The image is
regenerated by [`scripts/make_demo_svg.py`](scripts/make_demo_svg.py), which
downloads that PDF and runs the same code the CLI runs.
[catalog]: https://webdosya.csb.gov.tr/v2/yfk/2026/01/1-BF-202619011535-20260119155143.pdf
The **grade** (excellent/good/fair/poor) is a glanceable confidence signal, the
way camelot exposes accuracy: below `good`, review the pages before trusting the
output. `--json` includes `price_parse_rate` and `grade` per parse.
Or from Python:
```python
from acikpoz import parse_catalog
result = parse_catalog("bf2026.pdf", pages=range(8, 20))
for p in result.pozes:
if p.is_priced:
print(p.poz_no, p.birim, p.fiyat)
print(result.to_dict()["counts"]) # priced / group_headers / price_gaps
```
## How it works
1. **Rows.** Words are grouped into visual rows by vertical position (a few points of
tolerance), then sorted left-to-right.
2. **Price column, from the header.** The `Fiyat` header word on the right (x > 400) gives
the price column's x. A stray "fiyat" in a left-column description can't be mistaken for
it.
3. **Cells.** For each poz row: the leftmost cell is the poz code; the price is the
number-shaped token nearest the price-column x; the unit sits just left of it; the rest is
the description. Indented continuation lines extend the running description (and can carry
a price that spilled over).
4. **Group headers.** A price-less poz whose description ends in `:` is a category title —
flagged, not treated as a gap.
## Compare two catalog years
Catalogs are reissued regularly; the question estimators and auditors track by hand is *how
did this year's rates move from last year's?* `acikpoz diff` answers it — it joins two years
by poz code and classifies each change:
```
acikpoz diff bf2025.pdf bf2026.pdf --pages 8-400
acikpoz diff bf2025.pdf bf2026.pdf --tolerance 1 # hide sub-1-TL rounding noise
acikpoz diff bf2025.pdf bf2026.pdf --json
```
It reports price moves (with Δ and %Δ), added and removed pozes, unit changes, and pozes
that gained or lost a printed price — plus the **mean price %-change** for the year. As far
as the research found, no other open tool does year-over-year diffing for ÇŞB catalogs.
## Validate a parse
Before a parsed catalog feeds a cost estimate, it helps to know it is clean. `acikpoz
validate` runs deterministic quality rules over the pozes — no ML, no fixing, only surfacing:
```
acikpoz validate bf2026.pdf --pages 8-400
acikpoz validate bf2026.pdf --json
```
It flags duplicate poz codes, malformed codes, non-positive prices (**errors**), and priced
pozes with no unit or a unit outside the known set (**warnings**). It exits non-zero on any
error, so it can gate a pipeline (`acikpoz validate … && build-estimate`). This is also how
acikpoz stays honest about its own limits: in sections that print the unit once on a group
header and let the rows inherit it (Sıhhi Tesisat), per-row unit detection is weak, and
`validate` says so rather than hiding it. Price, poz code and description stay reliable.
## Using acikpoz with AI agents
An MCP server (`pip install 'acikpoz[mcp]'`, then `acikpoz-mcp`) exposes three tools:
`parse_catalog` (a PDF → structured pozes with honest coverage), `diff` (two catalog years
→ classified changes), and `validate` (a PDF → quality findings). The agent gets structured
data back, not prose it has to parse. Pair it with ihalent and an agent can reason across
both a tender's result *and* the unit prices it was measured against.
```jsonc
// e.g. Claude Desktop / Claude Code mcp config
{ "mcpServers": { "acikpoz": { "command": "acikpoz-mcp" } } }
```
## Scope, honestly
- **It reads the standard catalog layout.** The header-driven column detection handles the
common single- and two-price-column pages well; an unusual layout may leave more gaps —
which it reports rather than papering over. If a section parses badly, that's a bug worth a
sample.
- **It is a parser, not a price database.** It does not bundle or redistribute the catalog.
You bring the official PDF; acikpoz turns your copy into data.
- **Prices are nominal, as printed.** No inflation adjustment is baked in — that's an
analysis choice the caller makes knowing the year.
## Data, and why the PDFs aren't here
The ÇŞB catalogs are official public documents, but this repository does not redistribute
them: it ships the parser and nothing else, and `.gitignore` keeps `*.pdf` out. Point acikpoz
at the catalog you obtained from the official source. This is the same line ihalent draws —
own the tool, not the data.
## How this project is built
I'm an industrial engineer working in construction; I read these catalogs. I designed the
parsing approach — geometry over machine learning, honest gaps over invented numbers — and I
review every line; I use AI agents heavily for implementation speed, and the commit trailers
say so. The contract is the tests: they encode the exact word geometry a real page emits,
including the two-column Sıhhi Tesisat trap and the group-header rule, so green tests mean the
parser handles the real thing.
## Related
- **[ihalent](https://github.com/gulmezeren2-byte/ihalent)** — the other half of the Turkish public-procurement picture. `acikpoz` reads the unit-price catalogs that say what work *should* cost; `ihalent` reads the tender result notices that say what it *was* awarded for, and at what discount. Same discipline: every figure traceable to its source, nothing invented.
More tools by [Eren Gülmez](https://github.com/gulmezeren2-byte?tab=repositories).
## License
MIT — see [LICENSE](LICENSE).