The whole problem goes away when the text-replacement works correctly.
The problem here is how Word stores texts in different text runs. Not only different formatting splits text in different text runs, also marking grammar and spelling check problems do and multiple other things. So one can impossible predict how a text gets split into text runs when typed in Word. That's why your text-replacement approach is not good.
Apache POI provides TextSegment to solve those kind of problems. And using current apache poi 5.2.0
this also seems to work correctly. Former versions had have bugs in XWPFParagraph.searchText
- see Apache POI: ${my_placeholder} is treated as three different runs for a workaround.
Using TextSegment
one can determine the begin and end of a seached text and so doing the text-replacement better.
Following example should show this.
My Reference.docx
looks like so:

There ${firstname}
, ${lastname}
and ${address}
in head are bookmarked as firstname
. lastname
and address
. And their occurences in text are references as { REF firstname }
, { REF lastname}
and { REF address}
After running following code:
import java.io.*;
import org.apache.poi.xwpf.usermodel.*;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.*;
public class WordReplaceTextSegment {
static public void replaceTextSegment(XWPFParagraph paragraph, String textToFind, String replacement) {
TextSegment foundTextSegment = null;
PositionInParagraph startPos = new PositionInParagraph(0, 0, 0);
while((foundTextSegment = paragraph.searchText(textToFind, startPos)) != null) { // search all text segments having text to find
//System.out.println(foundTextSegment.getBeginRun()+":"+foundTextSegment.getBeginText()+":"+foundTextSegment.getBeginChar());
//System.out.println(foundTextSegment.getEndRun()+":"+foundTextSegment.getEndText()+":"+foundTextSegment.getEndChar());
// maybe there is text before textToFind in begin run
XWPFRun beginRun = paragraph.getRuns().get(foundTextSegment.getBeginRun());
String textInBeginRun = beginRun.getText(foundTextSegment.getBeginText());
String textBefore = textInBeginRun.substring(0, foundTextSegment.getBeginChar()); // we only need the text before
// maybe there is text after textToFind in end run
XWPFRun endRun = paragraph.getRuns().get(foundTextSegment.getEndRun());
String textInEndRun = endRun.getText(foundTextSegment.getEndText());
String textAfter = textInEndRun.substring(foundTextSegment.getEndChar() + 1); // we only need the text after
if (foundTextSegment.getEndRun() == foundTextSegment.getBeginRun()) {
textInBeginRun = textBefore + replacement + textAfter; // if we have only one run, we need the text before, then the replacement, then the text after in that run
} else {
textInBeginRun = textBefore + replacement; // else we need the text before followed by the replacement in begin run
endRun.setText(textAfter, foundTextSegment.getEndText()); // and the text after in end run
}
beginRun.setText(textInBeginRun, foundTextSegment.getBeginText());
// runs between begin run and end run needs to be removed
for (int runBetween = foundTextSegment.getEndRun() - 1; runBetween > foundTextSegment.getBeginRun(); runBetween--) {
paragraph.removeRun(runBetween); // remove not needed runs
}
}
}
public static void main(String[] args) throws Exception {
XWPFDocument doc = new XWPFDocument(new FileInputStream("./Reference.docx"));
String[] textsToFind = {"${firstname}", "${lastname}", "${address}"}; // might be in different runs
String[] replacements = {"Axel", "Richter", "Somewhere in Germany"};
for (XWPFParagraph paragraph : doc.getParagraphs()) { //go through all paragraphs
for (int i = 0; i < textsToFind.length; i++) {
String textToFind = textsToFind[i];
if (paragraph.getText().contains(textToFind)) { // paragraph contains text to find
String replacement = replacements[i];
replaceTextSegment(paragraph, textToFind, replacement);
}
}
}
FileOutputStream out = new FileOutputStream("./Reference_output.docx");
doc.write(out);
out.close();
doc.close();
}
}
The Reference_output.docx
looks like so:

All replacements are done and the bookmarks and also the references to the bookmarks are still there.