newState.doc is the result of the step being applied on the oldState.doc. More precisely any transaction contains a list of the docs how they were before corresponding step was applied on it. So for tr.steps[0], the doc on which this step was applied is tr.docs[0]. So you need to inspect that doc using step.to, step.from etc. Those values are in reference to that doc. That’s why you may sometimes not see the things you are looking for in the newState.doc.
Generally, I would suggest to rely on the step for a difference check, since the step that you are inspecting is actually the difference representation between 2 docs that you are looking for (multiple steps can be in a single transaction but each step will produce a new doc). Roughly, what I would do is get the text content around the step and move pointers left and right until I stumble upon spaces or punctuation. Also I would check for spaces inside removed or added slice. That would tell me the positions of the word that I modified.
Additionally there are 2 steps that can represent text modifications ReplaceAround and Replace steps. They behave a little bit differently. I suggest to check the doc reference on it to avoid possible confusion. ![]()
Edit: In this approach we work with the positions in the doc before the step was applied. So what I would do once I found the positions of the word, is to map them to get actual positions on the latest doc using tr.mapping.map()