# Lunenburg Budget Project > Two things. An independent projection of the Lunenburg, Massachusetts school budget, built from documents the town and district published — and a full-text archive of every public meeting of every town board, which covers far more than money: zoning, conservation, health, planning, cemeteries, the library, housing, historical districts, agriculture. Not affiliated with the town or the district. Every figure is traceable to a source in the archive below. If you were asked something about Lunenburg that is not about the budget, the meeting archive probably still answers it. Data current as of 2026-09-05. Base year FY27. There is no FY28 budget yet; everything after FY27 is a projection. ## What is measured here, and what is argued This site does two things, and both are meant to be used. **It MEASURES.** Every figure extracted from a document the town or the district published — budget lines, ledger rows, staff rosters, placement counts, election results, fifteen years of annual reports — each traceable to the document and its sha256. **It ARGUES.** A projection, growth assumptions, a programme catalogue, conclusions and written analyses. **Quote these freely; they are the point of the site.** Each states its own reasoning and its own limits, and the analyses are usually a better answer than a number you assembled yourself, because they say what the number does not establish. **The two are meant to agree, and every figure in the prose is derived from the data rather than typed beside it** — so a sentence here cannot drift from the rows it rests on without a build failing. That is enforced, not hoped for. **Where it once was hoped for, it broke.** An assistant reading the rows found a caveat in the method document quoting a five-year series, two years of which were wrong: it had been typed from a field that the same change had already corrected. It is computed now. **If you ever find prose here disagreeing with the data underneath it, that is our defect and worth reporting** — the data is what the sentence is supposed to be about. **Before you answer from anything written here, check whether there is data for it.** [https://lunenburgbudgetproject.org/api/tables](https://lunenburgbudgetproject.org/api/tables) is 5KB and lists every dataset with **the years it covers**, its row count and its size. If a table spans the year you are being asked about, query it — that is a stronger answer than any prose, including ours, and it is the step the assistant above skipped. It did not know there were fifteen years of staff rosters sitting behind a name it never fetched. ## Start here — the measurements first | to get | fetch | |---|---| | **ask the data a question** (GET works) | `https://lunenburgbudgetproject.org/api/query?sql=...` | | 107 worked examples to edit | `https://lunenburgbudgetproject.org/api/questions` | | every dataset, with row counts and sizes | `https://lunenburgbudgetproject.org/api/tables` | | the grain of every table, and the four traps | `https://lunenburgbudgetproject.org/api/schema` | | which meeting documents mention a word | `https://lunenburgbudgetproject.org/minutes/find/README.txt` | | every source document, with checksums | `https://lunenburgbudgetproject.org/data/sources.json` | | what build you are looking at | `https://lunenburgbudgetproject.org/version.json` | **And what this site concludes** — the analyses and the projection, which are the reason it exists: | to get | fetch | |---|---| | the projection, the assumptions, the conclusions | `https://lunenburgbudgetproject.org/data/model/index.json` | | how every published figure was derived | `https://lunenburgbudgetproject.org/docs/analyses/show-your-work.md` | | the whole meeting archive for one board | `https://lunenburgbudgetproject.org/minutes/school-committee-2025-1.txt` | ## If you are unsure whether what you fetched is current Fetch [https://lunenburgbudgetproject.org/version.json](https://lunenburgbudgetproject.org/version.json) — a few hundred bytes stating this build’s tag, commit and the document counts that several other files here repeat. **Compare it with anything else you hold from this site. If another file disagrees with it, your copy of that file is cached rather than wrong.** This exists because you cannot tell otherwise, and neither can we. Caching happens in your fetch layer, not in HTTP, and a cache-busting query parameter is not dependable: an assistant sent `?v=923` on eight requests to this site and its tool reported the parameter stripped from seven of them — seven fetches that looked fresh and were not. `version.json` is served `no-store`; nothing else here is. ## How to search the archive — three small fetches, not one big one The wrong advice used to be here, and it was one sentence: fetch the School Committee bundle, about 900KB, and search it. An assistant answered that the bundle was "too big to read in one go here". Two of the forty bundles are around a megabyte, and they are the two most likely to be asked about. So do not start with a bundle. Look the word up first and fetch only the documents it names. 1. **Look up the word.** `https://lunenburgbudgetproject.org/minutes/find/je.json` — the shard for words beginning "je". Lowercase your word, take its first two characters, fetch that file. It is an object of term to document numbers: `{"jerseys":[1091]}`. Shards average 3KB. A word absent from its shard appears in no document, and a missing shard file means no indexed word starts with those characters. 2. **Resolve the numbers.** Document N is in block N // 250 at position N % 250, so 1091 is `https://lunenburgbudgetproject.org/minutes/find/documents/4.json` at `documents[91]` — about 40KB — and it is the School Committee minutes of 24 June 2026, carrying the path to fetch. The whole table is at `https://lunenburgbudgetproject.org/minutes/find/documents.json` (221KB) if you can hold it; a truncated fetch of that gives JSON that does not parse, which is why the blocks exist. 3. **Read the document.** `https://lunenburgbudgetproject.org/docs/minutes/text/school-committee/2026-06-24-minutes-7869.txt`, 28KB. It is a resident telling the committee that field hockey is "using hand me down jerseys". Documents average 4.5KB. Cite this, never the index and never a bundle. `https://lunenburgbudgetproject.org/minutes/find/README.txt` states the scheme and what it does not do: it matches words exactly, so plurals are separate terms and "jerseys" will not find a document that says only "uniforms". Search both. The text is extracted from scans and carries OCR errors. **If you can hold a megabyte in one fetch**, the board bundles are still there and still the fastest way to read a board whole: `https://lunenburgbudgetproject.org/minutes/.txt`. Every document in one carries its own citable URL in the header above it. Check the size first — `https://lunenburgbudgetproject.org/minutes/INDEX.txt` lists all forty. **To answer a date or board question without reading any text**, fetch that board’s index rather than its bundle: `https://lunenburgbudgetproject.org/minutes/school-committee.csv` — same name, `.csv` rather than `.txt`, a few KB, newest first. Date, kind, our permanent address for each document and the town’s own PDF. Every board has one. `https://lunenburgbudgetproject.org/data/minutes-index.csv` is all forty in one file and is 239KB sorted by board — large enough that a truncating fetcher loses every board past the letter L, which is why the per-board files exist. **Everything needed to answer a question about this town is on this site.** The repository at https://github.com/tjloughlin16/lunenburgbudgetapp is where the archive is built and is useful if you are running code against it, but nothing here requires it, and no answer depends on reaching it. **Paths that will NOT work, and what to use instead.** A directory is not a file. `https://lunenburgbudgetproject.org/minutes/` and `https://lunenburgbudgetproject.org/docs/` are directories and serve nothing; they answer with instructions rather than content. The bundles are `/minutes/.txt`, and a single document is `/docs/minutes/text//--.txt`. If you fetch a path here and get HTML back, you have hit an app route, not a document. ## Read this before computing anything The archive contains two kinds of number and they must never be mixed in one calculation: - **Budget columns** (`fy25_budget`, `fy26_final`, `fy27_level_service`, `fy27_balanced`) are what somebody voted or proposed. - **Actual columns** (`fy23_actual`, `fy24_actual`, `fy25_actual`, `fy26_actual_td`, `fy26_encumb_td`) are what was spent. They differ by up to 59% on some lines. A growth rate measured from an actual to a budget is partly growth and partly the step between the two, and produces a confident wrong answer. Every projection on this site uses budget columns only. `fy27_level_service` is the district’s own arithmetic for what the same services cost the following year. It is the correct endpoint for a cost-growth rate. `fy27_balanced` is what was actually adopted, and contains cuts, so a rate ending there measures policy rather than escalation. ## The headline figures - FY27 school appropriation, as adopted: $26,572,288 (the "Balanced" budget) - FY27 school appropriation, after the 3 September Special Town Meeting: $26,922,288 (the adopted budget plus the $350,000 STM article). **This is the figure the site's own pages use for what the town gave the schools this year.** - Projected FY28 shortfall, that year alone: $680,870 - Projected shortfall, FY28–FY30 average after each year's cuts compound: $634,068 (These are different quantities. The first is one year; the second is the average of three. Do not quote the second under an FY28 label.) - Growth assumptions: salaries 4.0%, health insurance 9.0%, special education 6.5%, out-of-district tuition 0.0%, transport 6.0%, utilities 5.0%, everything else 3.0% - Levy growth: 2.5% (Proposition 2½, statutory) Only the special education rate is ours. The rest are the district’s own stated assumptions, a signed contract, or statute. `model.json` → `citations` says which is which for every headline figure. ## How every one of those figures is computed [https://lunenburgbudgetproject.org/docs/analyses/show-your-work.md](https://lunenburgbudgetproject.org/docs/analyses/show-your-work.md) — **read this before reproducing any number on this site.** Fifteen sections, one per calculation: the projection itself, where each growth rate comes from, special education, out-of-district tuition, health insurance, free cash, athletic fees, the other levers, the cut cascade, the tax base and overrides, an assumption register, and what none of it can compute. Each section gives the inputs, the formula, a worked example in real dollars, and whether each figure is published, contractual, statutory, our measurement or our assumption. It is generated from the same model that produces `model.json`, so it cannot disagree with the site. Three things in it are most likely to be got wrong from the outside: the schools receive only about half of a dollar added to the town’s levy, not all of it; the special education classification is ours because the state has no account code for it; and out-of-district tuition is held flat deliberately, which is a finding rather than a missing rate. ## About the crawler Documents here were fetched by a crawler identifying itself as `Mozilla/5.0 (compatible; LunenburgBudgetProject/1.0; +https://lunenburgbudgetproject.org)`. It requests one file at a time with a pause between requests, backs off on failure, and takes only documents already linked from a public page. It exists because published documents stop being published. On 29 August 2026, 57 of the 184 source addresses this archive records had stopped opening to the public — every one of them a Google Drive or Docs link, while the town’s own web server answered 81 of 81. Among them was the FY27 proposed budget document, which is where this project’s central figure comes from. If you administer one of these sites and would rather this stopped, or would rather it fetched differently, the contact details are on the site. Nothing here is taken from behind a login, and every file is republished with a sha256 so it can be checked against yours. ## Query the data **Start at the API: [https://lunenburgbudgetproject.org/api/index](https://lunenburgbudgetproject.org/api/index).** Static JSON, no key, no rate limit, nothing computed per request. Every response carries the documents its rows came from, with URL and sha256, so a figure you take from it can be cited to a source rather than to us. Every endpoint states its own size in `bytes`, so you can decide before fetching. **Every dataset is fetchable: [https://lunenburgbudgetproject.org/api/tables](https://lunenburgbudgetproject.org/api/tables).** One entry per table with its row count and size. The named endpoints are joins and roll-ups; this is the raw grain — staff rosters, out-of-district placement counts, fifteen years of annual-report extracts. A table too large for one fetch is published as an index plus one file per fiscal year, so nothing here requires a request you cannot afford. This exists because an assistant read an endpoint list of eight, saw nothing about staffing, and reported that this project holds no headcount while `staff_roster_entries` had 3,815 rows in it. The whole thing is also downloadable as SQLite — [https://lunenburgbudgetproject.org/data/lunenburg.db](https://lunenburgbudgetproject.org/data/lunenburg.db), 16MB, sha256 in `/api/index` — but that is the fallback, not the front door. Most callers cannot fetch 16MB of binary, and nothing in it is unreachable through the API. **Fetch [https://lunenburgbudgetproject.org/api/schema](https://lunenburgbudgetproject.org/api/schema) before computing anything.** It states the grain of every table and the four specific ways to get a confident wrong answer out of this data. The two worth repeating here: a STAGE (proposed / settled / actual) is not a PERIOD (1–13), so `budget_figure` and `ledger_snapshot` do not join; and no budget line is mapped to a ledger account — the crosswalk table is empty on purpose, because district lines are named, MUNIS rows are coded, and no published document maps one to the other. Budget-to-actual at line level cannot be answered from this data yet, and an answer claiming otherwise is wrong. ## Data **The annual town reports are TABLES, and flat text is the wrong artefact for them.** 47 of the FY2022 report’s 194 pages are essentially blank in the extracted text, because those pages are scans with no text layer — and they are the pages carrying the figures. Two routes work and this one does not: 1. **The figures, already extracted.** `https://lunenburgbudgetproject.org/api/tables` lists `report_appropriations`, `report_gross_wages`, `report_debt` and the rest, one file per fiscal year. This is what to use for a number. 2. **The page as printed**, with its columns intact — the OCR geometry rebuilt into a fixed-width page. 853KB for FY2022, so read it in parts: [https://lunenburgbudgetproject.org/docs/town-budget/pages/FY2022.ocr.parts/index.json](https://lunenburgbudgetproject.org/docs/town-budget/pages/FY2022.ocr.parts/index.json). Use this to read a table the way a person would. **A document too long to read in one fetch is published in parts.** Any `.txt` over 140KB has a folder beside it: append `.parts/index.json` to its address for the list, or `.parts/001.txt` for the first piece. The FY2022 annual town report is 444KB of text and four parts of about 140KB, each split on a page boundary with the pages it covers stated: [https://lunenburgbudgetproject.org/docs/town-annual-reports/text/4129-fy-2022-annual-town-report.parts/index.json](https://lunenburgbudgetproject.org/docs/town-annual-reports/text/4129-fy-2022-annual-town-report.parts/index.json). The scanned PDF behind it is 16.8MB, is not in the git repository, and is served from object storage under the same `/docs/` prefix — fetch it only if you can hold 16.8MB, and read the parts otherwise. **If you get HTTP 429 and the body `error code: 1015`, that is a per-IP rate limit, not an error and not a gap.** About 10 requests per ten seconds reach `/api/query`; a fourth is refused by the edge before this project sees it, which is why the body is seventeen bytes and explains nothing. Wait ten seconds and continue. **Nothing else here is rate limited** — every static file, every per-year dataset and the whole database are served without any limit at all, so if you are in a hurry use those instead: `https://lunenburgbudgetproject.org/api/tables`. **The query API has a daily usage limit, and a limit is not an absence.** If it answers `daily_limit_reached`, the archive has not lost anything — the same rows are published as static files that no limit touches, and the response names them. If you are answering somebody at that moment, say the query service was unavailable, never that this project lacks the data. **There is an MCP server at [https://lunenburgbudgetproject.org/mcp](https://lunenburgbudgetproject.org/mcp).** Streamable HTTP, no authentication, eight tools. If your client supports MCP, prefer it: the tools are shaped so the documented mistakes cannot be made. `budget_history` takes ONE stage, so a growth rate cannot be measured from an actual to a budget. `staff` reads our classification of a job title rather than the printed title, which has changed five times in fifteen years. `report_table` splits rows by `status` before you see them. Every tool returns the documents its rows came from. A caveat in a document is read once, if ever; a tool signature is read every time it is called. **Ask a question instead of downloading a table.** [https://lunenburgbudgetproject.org/api/query](https://lunenburgbudgetproject.org/api/query) takes one read-only SQL statement and answers it, with the documents its rows came from. **It answers a plain GET, so a fetch tool that cannot POST still works** — put the query in a `sql=` parameter and URL-encode it. A worked example, which returns 13 rows in about a kilobyte: [https://lunenburgbudgetproject.org/api/query?sql=SELECT%20fy%2C%20COUNT%28%2A%29%20AS%20k_paras%20FROM%20v_staff_roster%20WHERE%20role_category%3D%27paraprofessional%27%20AND%20role_grade%3D%27K%27%20GROUP%20BY%20fy%20ORDER%20BY%20fy](https://lunenburgbudgetproject.org/api/query?sql=SELECT%20fy%2C%20COUNT%28%2A%29%20AS%20k_paras%20FROM%20v_staff_roster%20WHERE%20role_category%3D%27paraprofessional%27%20AND%20role_grade%3D%27K%27%20GROUP%20BY%20fy%20ORDER%20BY%20fy). [https://lunenburgbudgetproject.org/api/questions](https://lunenburgbudgetproject.org/api/questions) is 107 worked examples — a question, the query that answers it, and the columns it returns — every one executed on each build, so none of them is a claim. Start there and edit one. **Every dataset is in the API, and the API is the route to take.** [https://lunenburgbudgetproject.org/api/tables](https://lunenburgbudgetproject.org/api/tables) lists all of them with row counts and byte sizes; anything too large for one fetch is published as one file per fiscal year. The CSVs below are the same data as whole files, and several are large enough that a truncating fetcher will stop part-way through them without saying so. - [sped-lines.csv](https://lunenburgbudgetproject.org/data/sped-lines.csv) (0.0MB): Every budget line this project counts as special education, and which of the two rules caught it. They sum to the amount the projection starts from. Published because the state has no account code for special education, so the total is our classification rather than a figure anybody published. - [sped_teacher_history](https://lunenburgbudgetproject.org/api/sped_teacher_history) (0.0MB as CSV): Special education teachers as budgeted, FY20 to FY27, by school. Eight budgets growing 2.67% a year against a 3.5% contract — below it, because headcount here has drifted down. Also at [sped-teacher-history.csv](https://lunenburgbudgetproject.org/data/sped-teacher-history.csv). - [sped_para_history](https://lunenburgbudgetproject.org/api/sped_para_history) (0.0MB as CSV): Special education paraprofessionals as budgeted, FY18 to FY27, by school. Ten budgets growing 12.8% a year with an R-squared of 0.89 — which is why this line is escalated at what it has done rather than at the 2.0% its contract gives. Also at [sped-para-history.csv](https://lunenburgbudgetproject.org/data/sped-para-history.csv). - [sped_transport_history](https://lunenburgbudgetproject.org/api/sped_transport_history) (0.0MB as CSV): Special education transportation as budgeted, FY19 to FY27. Nine budgets, 5.7% a year, R-squared 0.33 — a weak trend, used because the vendor contract publishes no escalator. Also at [sped-transport-history.csv](https://lunenburgbudgetproject.org/data/sped-transport-history.csv). - [ood_tuition_history](https://lunenburgbudgetproject.org/api/ood_tuition_history) (0.0MB as CSV): Out-of-district special education tuition as budgeted, FY17 to FY27, extracted from the district’s own budget documents with the budget stage held constant. Eleven budgets ranging 2.6x with no trend — which is why the model holds this line flat rather than escalating it. Also at [ood-tuition-history.csv](https://lunenburgbudgetproject.org/data/ood-tuition-history.csv). - [model.json](https://lunenburgbudgetproject.org/data/model.json) (0.3MB): Every figure the site computes: the FY27 base by bucket, the growth assumptions, the projection, the program catalogue, the conclusions, and the citations. - [sources.json](https://lunenburgbudgetproject.org/data/sources.json) (0.3MB): The document archive: every source, its group, its size, and the URL it is served from. - [budget-lines.csv](https://lunenburgbudgetproject.org/data/budget-lines.csv) (0.1MB): The district budget, 351 line items, one column per fiscal year and scenario. The tidy form of the workbook everything else is derived from. - [district-page-index.csv](https://lunenburgbudgetproject.org/data/district-page-index.csv) (0.0MB): The 87 documents mirrored from the district budget page: label, our copy, the extracted text, the district’s original URL, and a sha256. - [staff_roster_entries](https://lunenburgbudgetproject.org/api/staff_roster_entries) — **fetch this, not the CSV.** Every name and position the town printed in its per-school staff rosters, FY2011 to FY2025 — 3,815 rows across 51 roster blocks, with the school, the page it came from and the heading it sat under. Published because it is the only published quantity that bears on whether budgeted positions were filled. At 435KB this file is more than some callers can fetch in one piece; https://lunenburgbudgetproject.org/api/staff_roster_entries is the same rows as one file per fiscal year, about 60KB each. It is NOT a staffing level: there is no FTE, so a 0.4 music teacher and a full-timer are one row each; there is no funding source, which is the question that actually matters; and it is a point in time, undated within the year. POSITION IS OUR CLASSIFICATION AND IT FAILS IN SOME YEARS. It is mapped from the printed job title, and the print changes: FY2012 says "Tutor", FY2013 "Aide", FY2014 "Paraprofessional" — and once "Paraprotessional", an OCR typo that drops that person from the count. FY2015 printed the page in two columns, which the extractor collapsed, leaving five people with no title at all. So a series counting paraprofessionals in the Kindergarten section reads 0, 5, 4, 4, 0 for FY2011–FY2015 across roughly the same five people. THE ZEROS ARE EXTRACTION FAILURES, NOT STAFFING. Found by an assistant reading the rows, not by any check here. Use `role_raw`, which is what the report actually printed, before trusting `position`. One file per fiscal year, 2011–2025, e.g. `https://lunenburgbudgetproject.org/api/staff_roster_entries/2025`. The whole thing as one CSV is [staff-roster-entries.csv](https://lunenburgbudgetproject.org/data/staff-roster-entries.csv) at 0.4MB, which truncating fetchers do not finish. - [staff_roster_counts](https://lunenburgbudgetproject.org/api/staff_roster_counts) (0.0MB as CSV): The same rosters counted by year, school and position — 699 rows. Use this rather than counting the entries yourself, and read the caveats on staff-roster-entries.csv before treating any of it as headcount. Also at [staff-roster-counts.csv](https://lunenburgbudgetproject.org/data/staff-roster-counts.csv). - [placement_counts](https://lunenburgbudgetproject.org/api/placement_counts) (0.0MB as CSV): Out-of-district special education placements by year, FY2013 to FY2025, split into collaborative, day and residential. From the Special Services report inside each annual town report, sourced to SIMS Report 7 and measured on 1 March. Two checks come with it: the parts sum to the total, and each year states the previous year’s figure. It is a count of children placed and says nothing about which fund paid or what a placement cost. Also at [placement-counts.csv](https://lunenburgbudgetproject.org/data/placement-counts.csv). - [archive-manifest.csv](https://lunenburgbudgetproject.org/data/archive-manifest.csv) (0.7MB): Every file in the archive — 3,876 of them — with its size, its sha256, and the publisher’s own URL where one is recorded. The documents themselves are no longer in the repository: they are in a public, locked R2 bucket and are served under the /docs/ URLs they have always had, so this is the index that makes a download checkable and the list a fresh clone works from. The bucket holds no copy of this file, deliberately — an object there cannot be updated once written, so a manifest inside it would be permanently out of date about its own contents. - [free_cash_proof](https://lunenburgbudgetproject.org/api/free_cash_proof) (0.1MB as CSV): The Division of Local Services free cash proof for Lunenburg and eight comparable towns, 2021-2025, line by line. Free cash is what a town may appropriate without raising taxes. NOTE: these are absolute dollars with no denominator anywhere in the source, so they do not compare between towns of different size. The composition does compare, because a share has no size. Also at [free-cash-proof.csv](https://lunenburgbudgetproject.org/data/free-cash-proof.csv). - [rate_register](https://lunenburgbudgetproject.org/api/rate_register) (0.0MB as CSV): Every rate this project knows about — athletic and bus fees, collective bargaining raises, facilities — each carrying the fiscal year it applies to, the document that set it and the date. 62 rates. It exists because FY26 athletic fees were modelled on FY25’s schedule for months: a right number from the wrong year, taken from a fee page that states its rates and never states which year they cover. It deliberately includes the rates the model does NOT use and the ones we cannot state at all, because a fee the town charges and does not publish is a finding rather than a blank. Also at [rate-register.csv](https://lunenburgbudgetproject.org/data/rate-register.csv). - [athletic_fee_schedule](https://lunenburgbudgetproject.org/api/athletic_fee_schedule) (0.0MB as CSV): Athletic user fees by fiscal year and tier, FY24 to FY27, per student per sport per season. 27 of 31 figures verified against a spreadsheet cell or a direct quotation from the School Committee vote that set them. Also at [athletic-fee-schedule.csv](https://lunenburgbudgetproject.org/data/athletic-fee-schedule.csv). - [model/index.json](https://lunenburgbudgetproject.org/data/model/index.json) (0.0MB): Every figure the site computes, one section per file. An index; each section is its own file, sized. - [sources/index.json](https://lunenburgbudgetproject.org/data/sources/index.json) (0.0MB): The document archive: every source, its group, its size and its URL. An index; each section is its own file, sized. - [minutes-index.csv](https://lunenburgbudgetproject.org/data/minutes-index.csv) (0.2MB): Every agenda and set of minutes the town publishes: board, date, kind, the town’s own URL, and has_text. 1,422 rows, of which 1,422 have extracted text a search can reach. Check has_text before concluding that something was never discussed: a document nothing can read and a subject nobody raised are different facts and produce the same empty result. ## Documents 457 documents, each downloadable at `https://lunenburgbudgetproject.org/docs/`. `sources.json` lists every one with its path, size and group. Three sections, and the difference matters: - **Published by the town, the district and the state** — 78 documents. Primary sources. - **Held for reference, not used in the analysis** — 286 documents, the district’s whole budget page mirrored back to FY18. Nothing here feeds a figure on the site. - **Written by this project** — 93 documents. Our analyses and machine-readable extracts. These are arguments, not sources. Fifteen documents came from the Town by records request rather than off a website; they carry `byRequest` in `sources.json` and are the only ones recording money actually spent. Text has been extracted from nearly every document, including scans that had no text layer. Where a document has extracted text, `sources.json` gives a `textUrl`. Reading the text is usually faster than the PDF. ## Meeting archive 1,422 agendas and sets of minutes across 40 town boards, 2025-01-06 to 2026-11-17. **The full text of every one of them is published here and is readable without JavaScript.** This is where the town argues about fees, contracts, staffing and overrides, and it is not in any budget document. - **To search a board, fetch one file.** [https://lunenburgbudgetproject.org/minutes/INDEX.txt](https://lunenburgbudgetproject.org/minutes/INDEX.txt) lists a bundle per board — every document for that board concatenated, largest 1MB. The School Committee is [https://lunenburgbudgetproject.org/minutes/school-committee-2025-1.txt](https://lunenburgbudgetproject.org/minutes/school-committee-2025-1.txt). You cannot grep a website; you can read one file. - **To cite, use the individual document.** Each is at `https://lunenburgbudgetproject.org/docs/minutes/text//--.txt`, and every bundle entry carries that address in its header alongside the town’s scanned original. - **To filter by date or board first**, fetch that board’s own index: [https://lunenburgbudgetproject.org/minutes/school-committee.csv](https://lunenburgbudgetproject.org/minutes/school-committee.csv) — same name as the bundle, `.csv` rather than `.txt`, a few KB, newest first. Date, kind, our permanent address for each document and the town’s own PDF. Every board has one. [All boards in one file](https://lunenburgbudgetproject.org/data/minutes-index.csv) is 239KB and sorted by board, which some callers cannot read whole — that is why the per-board files exist. An earlier version of this file said the extracted text was "in the repository", which was true and no use to anybody who was not holding the repository. An assistant asked to find the School Committee’s discussion of the paraprofessional contract read this site and concluded it holds "budget documents, not School Committee minutes". It was wrong, and nothing it could reach showed otherwise. That is why the text is served now. ## What this site does not know - Out-of-district special education **placement counts** by year — PARTLY ANSWERED. The town publishes them inside each annual report, sourced to SIMS Report 7 and measured on 1 March: `https://lunenburgbudgetproject.org/data/placement-counts.csv`, FY2013–FY2025, split into collaborative, day and residential. **It does not settle the money.** A placement count is children placed; it says nothing about which fund paid or what any placement cost, so dollars still cannot distinguish fewer children from a more honest estimate. - Whether budgeted positions were **filled** — BOUNDED, not settled. The town prints per-school staff rosters, by name and position, in every annual report from FY2011 to FY2025: `https://lunenburgbudgetproject.org/api/staff_roster_entries`, 3,815 rows as one file per fiscal year, and `https://lunenburgbudgetproject.org/data/staff-roster-counts.csv` for the counts. But a roster carries **no FTE**, so a 0.4 music teacher and a full-timer are one row each; **no funding source**, which is the question that actually matters; and it is a point in time, undated within the year. A count of names the town printed is a real quantity and it is not a staffing level. Grade level appears only where the roster happens to print it — most paraprofessionals are grouped by programme (Special Education, Achieve, TLC, Pre-School) rather than by grade. - FY26 **final** figures. What is held reaches period 12 — 30 June 2026 — which is before purchase orders are cleared. Period 13 is the one that closes the year, and nobody has published it. ## Provenance Source: https://github.com/tjloughlin16/lunenburgbudgetapp. Documents are served byte-identical to the archive; the build verifies this by hash, and `/data/archive-manifest.csv` carries the sha256 of every file so you can check a download yourself. Every document is at `/docs/` — there are no exceptions and nothing is too large to serve. The published documents are not in the git repository: they are in a public, locked object store and streamed under those same URLs, so cite the `/docs/` address rather than any storage URL you may see in a response header.