Topical Proximity Classifier: an LLM content audit tool for pruning decisions
Score how close every article on your site is to the services you sell, then decide what to keep, merge, prune and link. Run the free tool yourself, or have a custom classifier built for your site.
MIT licensed. Runs on your own OpenAI or Anthropic API key.
What is the Topical Proximity Classifier?
The Topical Proximity Classifier is an open-source tool built by Lum Kamishi that uses a large language model to judge how close each article on your website is to the core services or products you want to rank for. It reads every post against a description of your business and assigns it a relevance band, with a score and a written reason.
Core
The page directly covers a service you sell, a primary product or a primary entity your site needs to compete.
Adjacent
The page covers a supporting concept, answering an informational question that naturally routes readers toward a core page.
Distant
The page covers a subject that adds nothing to your main offering and dilutes the site's focus.
Topical proximity classifier vs keyword clustering
Keyword clustering and proximity classification solve different problems. Clustering maps search demand before you write. Proximity classification audits what you have already published.
| Method | What it measures | Input it needs | Decision it supports | Blind spot |
|---|---|---|---|---|
| Keyword clustering | Shared URLs in search results | A query list and SERP data | Brief planning and outlines | Ignores how existing pages relate to the business |
| TF-IDF content scoring | Term frequency against top competitors | One target keyword and a draft | On-page keyword optimization | Misses relevance across different URLs |
| Embedding cosine similarity | Distance between text vectors | Text and an embedding model | Raw semantic similarity | No business or buyer-intent context |
| Topical proximity classifier | Alignment to the services you sell | Post content, a business description and a services list | Pruning, merging and internal linking | Needs a careful business description and a human check on borderline posts |
The output
The classifier appends seven columns to your audit CSV. Sort by tier for the keep-or-prune pass, and use the other six as the audit trail behind each call.
| Column | What it tells you | Possible values |
|---|---|---|
service | Which service the post supports | A service name, or NONE |
service_alignment | How directly it supports that service | 0.0 to 1.0 |
post_type | Format and role of the post | SERVICE_INTENT, PRODUCT_REVIEW, COMPARISON, INSTALLATION_GUIDE, EDUCATIONAL, DESIGN_INSPIRATION, DRIFT, OFF_TOPIC |
commercial_intent | The reader's distance from a purchase | HIGH, MEDIUM, LOW |
tier | The pruning bucket | CORE_SERVICE, CORE_PRODUCT, ADJACENT_COMPARISON, ADJACENT_INFORMATIONAL, PERIPHERAL, OFF_TOPIC |
reasoning | Why the post got this call | Two to three sentences |
overridden | Whether the second pass changed the call | True or False |
How does the classifier work?
The model handles the judgment and a fixed rule set assigns the tier, so you get the model's reading of each post together with repeatable buckets.
1. It learns the business from two files
config.yaml describes what the business does, where it operates and what it doesn't offer. services.csv lists each service with a short descriptor. Both go into every prompt, so each post is judged against this business's catalog, not a general idea of the topic.
2. It reads the body as well as the title
When the audit CSV has no Content column, the script fetches each URL and extracts the main text with trafilatura, up to a set character limit. A title alone gives too little to judge from: "SunPower Maxeon Panels Review" could be a 3,000-word technical breakdown or a 400-word affiliate post.
3. It classifies each post with structured output
Each post reaches the model with the business context and six worked examples that anchor the hardest boundaries, such as product review against service intent and drift against off-topic. The model reasons first, then returns five fields: reasoning, service, service_alignment, post_type and commercial_intent. The schema is enforced through strict response_format on OpenAI and a tool_use input schema on Anthropic, so the model can't answer in free text or add fields of its own.
4. Fixed rules turn the fields into a tier
The script maps post_type, service_alignment and commercial_intent to a tier with fixed rules, so identical fields always produce an identical tier. A SERVICE_INTENT post with high or medium commercial intent lands in CORE_SERVICE, and a post that supports none of the services lands in OFF_TOPIC whatever its score.
if post_type == "OFF_TOPIC" or service == "NONE": OFF_TOPIC
if post_type == "SERVICE_INTENT" and intent in (HIGH, MEDIUM): CORE_SERVICE
if post_type == "PRODUCT_REVIEW" and alignment >= 0.5: CORE_PRODUCT
if post_type == "COMPARISON" and alignment >= 0.5: ADJACENT_COMPARISON
if post_type in (EDUCATIONAL, INSTALLATION_GUIDE) and alignment >= 0.4: ADJACENT_INFORMATIONAL
if post_type == "DESIGN_INSPIRATION": ADJACENT_INFORMATIONAL if alignment >= 0.4 else PERIPHERAL
if post_type == "DRIFT" or alignment < 0.3: PERIPHERAL
# anything left: ADJACENT_INFORMATIONAL
5. A second pass rechecks the borderline calls
Every post with an alignment between 0.35 and 0.65 goes back to the model for an independent re-check, which either confirms the first call or replaces it. The overridden column records which happened. The band covers that range because single-pass calls in the middle of the scale are the ones most likely to be wrong.
Example output
Illustrative output for a residential solar installer selling installation, battery backup, EV chargers and maintenance. An embedding audit would score these posts as close neighbors, because all six share solar vocabulary.
| Post | Post type | Alignment, 0 to 1 | Intent | Tier | Action |
|---|---|---|---|---|---|
| How Much Does Solar Installation Cost in 2025? | SERVICE_INTENT | 0.95 | HIGH | CORE_SERVICE | Protect and refresh |
| Tesla Powerwall 3 Review: Is It Worth It? | PRODUCT_REVIEW | 0.70 | MEDIUM | CORE_PRODUCT | Keep and refresh |
| Monocrystalline vs Polycrystalline Solar Panels | COMPARISON | 0.65 | MEDIUM | ADJACENT_COMPARISON | Human review |
| How Solar Panels Actually Work: The Science | EDUCATIONAL | 0.45 | LOW | ADJACENT_INFORMATIONAL | Human review |
| 10 Beautiful Solar Roof Designs | DESIGN_INSPIRATION | 0.35 | LOW | PERIPHERAL | Remove if it earns nothing |
| Smart Thermostats for Energy Savings | DRIFT | 0.10 | LOW | OFF_TOPIC | Removal candidate |
Only the top two rows are safe to leave alone, and the two adjacent rows need a person to read them.
For a post on Powerwall installation cost, the reasoning reads: the post covers labor, permits and panel integration, which are core to the battery backup service, and the reader is a homeowner deciding whether to buy, so a quote request is the likely next step.
How to run the Topical Proximity Classifier
You need Python 3.10 or newer and an API key from OpenAI or Anthropic. Install into a virtual environment so the tool's dependencies stay out of your system Python.
1. Install
git clone https://github.com/lumkamishi/topical-proximity.git
cd topical-proximity
python3 -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -e .
2. Set your API key
export OPENAI_API_KEY="sk-..."
# or
export ANTHROPIC_API_KEY="sk-ant-..."
# Windows PowerShell: $env:OPENAI_API_KEY="sk-..."
3. Describe the business in config.yaml
Write the description the way you would brief a strategist, and state what the business doesn't sell as plainly as what it does, because exclusions drive the DRIFT and OFF_TOPIC calls. A solar installer that leaves out "we don't sell backyard solar lights" can see "Best Backyard Solar Lights" filed as a core product post.
4. List the services and the posts
services.csv holds one row per service, and audit.csv holds one row per post. URL is required, while Title, H1 and Meta Description are recommended. If a Screaming Frog crawl already gave you the body text, add it as a Content column and the fetch step is skipped.
5. Run it
topical-proximity \
--config config.yaml \
--services services.csv \
--audit audit.csv \
--output topical_proximity.csv
Model choice and cost per 500 posts
Provider and model are switched with two lines in config.yaml. These estimates from the project README include the verification pass:
| Provider | Model | Cost per 500 posts, USD | Runtime, minutes |
|---|---|---|---|
| OpenAI | gpt-5.4 | 3 to 5 | 5 to 15 |
| Anthropic | claude-opus-4-7 | 8 to 12 | 10 to 20 |
| Anthropic | claude-sonnet-4-6 | 3 to 5 | 5 to 15 |
| Anthropic | claude-haiku-4-5 | 1 to 2 | 5 to 10 |
Haiku suits a first pass on a large blog, while Sonnet or gpt-5.4 give better reasoning text for a client deliverable. Setting fetch_content: false and verify_alignment_range: [0, 0] roughly halves the cost, although accuracy drops with both.
Turning tiers into prune and link decisions
The classifier measures topical fit only. Before any removal, join its output with twelve months of Search Console clicks, GA4 key events and referring domains.
CORE_SERVICEandCORE_PRODUCT: keep, refresh the outdated ones, and never remove one without a review.ADJACENT_COMPARISONandADJACENT_INFORMATIONAL: read the reasoning column, then decide by hand.PERIPHERALandOFF_TOPIC: remove those with no clicks, conversions or links first, and redirect linked ones to the closest relevant page.
The tiers also give you an internal linking plan. Adjacent posts should link up to the core page for the service named in the service column, and links into removed posts should be taken out so crawl paths lead only to pages you keep. Allow two to three months before judging the result, and check again after the next Google core update.
Topical authority builds through that tighter focus. Topical map tools plan which pages to create, and this classifier decides which existing pages still belong in the map. The full method is in my content pruning guide.
What a custom topical proximity classifier costs
A custom classifier build is a fixed-scope technical project to clean up content debt and resolve cannibalization across a large URL inventory, built around your own topic map.
| Option | Price, USD | What you get | Best for |
|---|---|---|---|
| Free audit | $0 | An initial assessment of content debt and indexation problems | Checking the baseline structural issues |
| Hands-on teardown | $997 | A manual diagnostic of the site architecture with graded findings | Sites that need an action plan now |
| Custom topical proximity classifier build | $1,479 | The full classification pipeline, a topic map, a pruning roadmap and the code | Domains with hundreds of URLs that need a systematic cleanup |
| Typical agency engagement | $5,000+ | A monthly retainer covering mixed SEO tasks | Ongoing execution across off-page and content |
What a custom build delivers
- Topic map file: a structured taxonomy of your core commercial subjects, the concepts that support them, and the topics you explicitly don't cover.
- Per-URL proximity scores: every crawled URL with its alignment score, relevance band and primary entity label.
- Cannibalization report: the URLs that target the same topic and compete against each other in Google.
- Pruning roadmap: a spreadsheet listing which URLs to delete, which to redirect and which to rewrite.
- Internal link plan: source URLs, target URLs and vetted anchor text for each link.
For a home renovation client, we built a three-layer Python pipeline that combined search-value scoring, keyword cannibalization detection and GPT-based topical proximity classification, and produced a YAML topic map and a content pruning roadmap.
Every finding is prioritized by impact and effort, with high-impact cleanup listed first so your editors and engineers can start on the structural problems straight away. You also get the full pipeline code at delivery, so your team can rerun it against new pages and keep editorial work inside your strategic scope.
Honest answers about the Topical Proximity Classifier
Who is it for?
SEO consultants running client blog audits, in-house SEO leads at content-heavy businesses, and agencies selling fixed-scope audits who need keep-or-prune calls across hundreds of posts.
Which sites does it suit?
The taxonomy assumes a service or product business, such as an installer, law firm, contractor or SaaS company with a service layer. Affiliate sites and pure publishers aren't covered yet, and pull requests with taxonomies for them are welcome.
How accurate is it?
Accuracy depends on the business description and the services list. Test it on 20 to 30 posts you already have a firm view on, read every reasoning column, and tighten the description until the calls match yours.
Is my content sent anywhere?
Each post's text goes only to the OpenAI or Anthropic API you configure, under that provider's API terms.
Does it use traffic or backlink data?
No. It reads content and business context only, so pair it with Search Console and a backlink tool before deciding anything.
Does it replace human review?
No. It handles the mechanical sorting, which leaves your reading time for the adjacent and borderline rows.
How is the custom build different from the free tool?
The free tool classifies posts against a services list you write yourself. A custom build adds a topic map built for your site, cannibalization detection, a pruning roadmap and an internal link plan, all prioritized by impact and effort.
Do I keep the code from a custom build?
Yes. You receive the full pipeline code at delivery, so your team can rerun it whenever new pages are published.
What do you need from me for a custom build?
Read access to Search Console, a crawl export of the site, and a clear list of what the business sells and doesn't sell. That list becomes the starting point for the topic map.
Hire me to run the content prune
My content audit scores every page on traffic, topical fit and quality, then hands you a keep, merge and remove list with a reason for each call. It draws on 11+ years of technical SEO on large sites.
See the content audit