--- /src/modules/swmodule_original_reconstructed.cpp	2026-03-07 19:43:54.208256844 +0000
+++ /src/modules/swmodule_modified.cpp	2026-03-07 19:43:21.832197129 +0000
@@ -385,6 +385,17 @@

 	listKey.clear();
 SWBuf term = istr;
+// Normalize typographic apostrophe (U+2019, UTF-8: 0xE2 0x80 0x99) to standard apostrophe
+// so that French searches work regardless of which apostrophe the user types
+{
+    std::string normalizedTerm = term.c_str();
+    size_t pos = 0;
+    while ((pos = normalizedTerm.find("\xe2\x80\x99", pos)) != std::string::npos) {
+        normalizedTerm.replace(pos, 3, "'");
+        pos += 1;
+    }
+    term = normalizedTerm.c_str();
+}
 bool includeComponents = false;	// for entryAttrib e.g., /Lemma.1/

 	// this only works for 1 or 2 verses right now, and for some search types (regex and multi word).
@@ -537,7 +548,13 @@
 			Xapian::Query q = queryParser.parse_query(istr);
 			Xapian::Enquire enquire = Xapian::Enquire(database);
 #elif defined USELUCENE
-			q = QueryParser::parse((wchar_t *)utf8ToWChar(istr).getRawData(), _T("content"), &analyzer);
+			// Append a trailing space to work around a CLucene tokenizer bug where
+			// the last token is not finalized correctly without a following whitespace.
+			// This fixes: single +TERM searches, and some Unicode words (e.g. Greek LXX).
+			SWBuf istrFixed = istr;
+			istrFixed.append(' ');
+			q = QueryParser::parse((wchar_t *)utf8ToWChar(istrFixed.c_str()).getRawData(), _T("content"), &analyzer);
+
 #endif
 			(*percent)(20, percentUserData);

@@ -725,6 +742,16 @@
 			// phrase
 case -1: {
 				textBuf = stripText();
+				// Normalize typographic apostrophe (U+2019) in verse text to match normalized search term
+				{
+					std::string normalizedBuf = textBuf.c_str();
+					size_t pos = 0;
+					while ((pos = normalizedBuf.find("\xe2\x80\x99", pos)) != std::string::npos) {
+						normalizedBuf.replace(pos, 3, "'");
+						pos += 1;
+					}
+					textBuf = normalizedBuf.c_str();
+				}
 				if ((flags & REG_ICASE) == REG_ICASE) textBuf.toUpper();
 				sres = strstr(textBuf.c_str(), term.c_str());
 				if (sres) { //it's also in the stripText(), so we have a valid search result item now
@@ -741,6 +768,16 @@
 				int multiVerse = 0;
 				unsigned int foundWords = 0;
 				textBuf = getRawEntry();
+				// Normalize typographic apostrophe (U+2019) in verse text to match normalized search term
+				{
+					std::string normalizedBuf = textBuf.c_str();
+					size_t pos = 0;
+					while ((pos = normalizedBuf.find("\xe2\x80\x99", pos)) != std::string::npos) {
+						normalizedBuf.replace(pos, 3, "'");
+						pos += 1;
+					}
+					textBuf = normalizedBuf.c_str();
+				}
 				SWBuf testBuf;
 				// Here we loop twice, once for the current verse, to see if we have a simple match within our verse.
 				// This always takes precedence over a windowed search.  If we match a window, but also one verse within
