The keyword overlap example that settles the argument
2 pairs, both real:
"seo software" vs "seo tool" 5 of 10 results shared
"invoice ocr" vs "invoice financing" 0 of 10 results shared
Now rank those pairs by how similar the words look. "invoice ocr" and "invoice financing" share a whole word and a subject. "seo software" and "seo tool" share an acronym and disagree on the noun. Any string-similarity check, and most language models asked to eyeball it, will call the invoice pair the closer one.
The SERPs say the opposite, and the SERPs are what decides. Someone typing "seo tool" and someone typing "seo software" want the same list of products, so Google shows them the same list. Someone typing "invoice ocr" wants document-scanning software and someone typing "invoice financing" wants a loan against unpaid invoices. Near-identical words, completely different rooms.
That is why nothing in this tool is a model's opinion. Every number on the screen came out of a search that actually ran.
What counts as a cannibalization match
Each keyword's top 10 is reduced to a set of URLs, and each URL is normalized to hostname plus path, lowercased, with any trailing slash removed. So https://example.com/guide/ and http://example.com/guide count as one shared result, not 2, and query strings or tracking parameters cannot inflate the number.
The overlap for a pair is the size of the intersection of those 2 sets. Nothing is weighted by position and nothing is scored by title. A URL is in both top 10s or it is not.
3 shared results, and 7
2 thresholds, both borrowed rather than invented for this page.
3 or more shared URLs - the keywords compete. It is the standard SERP-overlap threshold, and the same constant our own content pipeline uses when it groups researched keywords into pages. Below 3, overlap is mostly noise: Wikipedia and a couple of large publishers rank for nearly everything, so 1 or 2 shared URLs between unrelated keywords is ordinary.
7 or more - Google is answering both with essentially the same page set. At that point there is no separate result to win. Publishing 2 pages splits your own internal links, your own external links and your own click history across 2 URLs that are fighting for one slot.
The results table shows each pair with its count out of ten and one of the actual shared URLs. That last column matters more than it looks: open the example, see a competitor's page ranking for both keywords, and the number stops being a score and becomes evidence.
Every keyword conflict, not a shortlist
25 keywords make 300 pairs, and all 300 get compared.
That is worth saying because the tempting shortcut is to compare only the pairs that look related. It saves nothing here, and it introduces the one failure mode that is hard to spot. A stem filter comparing "crm software" against "customer database" finds no shared word roots and drops the pair before it is checked. Those 2 keywords share a SERP. It is exactly the collision a human scanning the list also misses, which is the reason to run the check at all. Comparing all 300 pairs is free anyway: the cost is per keyword searched, not per pair compared.
One cluster is one page, not three decisions
Pairs are not the useful output. If "crm software" collides with "customer database", and "customer database" collides with "contact management system", reporting 3 separate pairs implies 3 separate decisions when there is only one: that is a single topic, and it needs a single page.
So the collisions are merged with union-find. Keywords connected through a chain of overlaps land in one cluster, and each cluster gets a primary - the member with the most shared results across all its connections. Write for the primary and work the rest of the cluster in as headings and natural variants inside that same page.
Keywords colliding with nothing else in the list are shown separately as safe to target on their own, and the pair table exports as CSV with the columns keyword_a,keyword_b,shared_top10,severity.
"Could not check" is not "no problem"
Some keywords come back with no search results at all - a fetch that failed, a term so obscure nothing indexed matches it. Those are pulled out and reported under Could not check, with a plain note that this means unknown and not safe.
That distinction separates a useful checker from a dangerous one. A keyword that was never checked, silently dropped into the "no collisions found" bucket, is a false all-clear, and a false all-clear is worse than no answer, because you act on it. You write the second page. Then you find out. Re-run anything in that list before planning around it.
What it does not know
It has no idea what is on your site. It compares the keywords in the list against each other, not against your published pages. If two of your posts are already trading places, put both of their target keywords into the list - that tells you whether those 2 keywords were ever separable, which is the actual question - but no site audit is happening here.
It also returns no search volume and no difficulty score, and results come from US English searches with no country selector. SERPs move slowly, so a keyword checked recently is reused rather than searched again.
The 25-keyword cap is there for the same reason as the human check: each keyword is a live search, and live searches cost money. Run your list in batches if it is longer, keeping any keyword you suspect of colliding in every batch. If you would rather not run this check by hand before every page you plan, that is the loop BlazeHive automates end to end.