Short answer, no single tool that does exactly that, at least not one I could find. But a few things get close, and honestly the DIY version is tiny.
The reason spellcheck whiffs on this is that hunspell and aspell look at one token at a time. Feed them “excel lence” and they see a real word followed by junk, so you get a squiggle under “lence” with suggestions like “lance” or “fence”. Nothing in there ever considers gluing it back onto the word in front of it. You need something that looks at pairs.
Stuff I found that actually does it:
LanguageTool is probably the best bet off the shelf. It has rules for wrongly split words and it works on token pairs instead of single tokens. Runs local as a server or through languagetool-commandline.jar. English coverage is decent but not complete. German is much better supported, which tracks, since split compounds are basically a national sport over there.
symspellpy has lookup_compound and word_segmentation. Dictionary plus frequency based, and it handles both bad splits and missing spaces. Closest thing to a library that just does your problem.
dehyphen, Python, comes out of the pd3f project. Built specifically for this on text pulled out of PDFs. It scores each join or no join call with a language model instead of a dictionary, so it does better on the ambiguous ones.
pd3f itself if your source is PDFs and you want the extraction pipeline to rebuild paragraphs properly on the way out.
Word and Google Docs will catch some of these in grammar check if it’s a short doc and you just want to eyeball it.
DIY version:
The logic is maybe six lines. Walk adjacent token pairs, and if the joined version is in the dictionary, flag it. Then the filter that kills basically all the false positives: only flag it when the second fragment is not a real word on its own. “excel lence” fires because “lence” is garbage. “may be”, “in to”, “a while” and “all ready” all stay put, and those are exactly the ones a dumb joiner would destroy.
For the approve or skip part, cheapest thing is to dump the candidates and run them through vim with :%s/old/new/gc. That gives you y/n/a/q per match for free. Or build the prompt into the script and add a --yes flag for when you don’t feel like babysitting it.
One thing on scope though. Tabs and newlines are the easy case. A word broken across a line ending, especially with a trailing hyphen, is a different problem than a stray space in the middle of a line, and you’re usually better off fixing that at extraction time than trying to fix it after the fact.