diff --git a/src/kde/kioslave/kio_recoll/htmlif.cpp b/src/kde/kioslave/kio_recoll/htmlif.cpp index 083be203..d31cd62c 100644 --- a/src/kde/kioslave/kio_recoll/htmlif.cpp +++ b/src/kde/kioslave/kio_recoll/htmlif.cpp @@ -204,24 +204,32 @@ void RecollProtocol::queryDetails() class PlainToRichKio : public PlainToRich { public: PlainToRichKio(const string& nm) - : PlainToRich() , m_name(nm) + : m_name(nm) { } - virtual ~PlainToRichKio() {} + virtual string header() { if (m_inputhtml) { - return snull; + return cstr_null; } else { - return - string("" - "") + - m_name + - string("
");
+	    return string(""
+			  "").
+		append(m_name).
+		append("
");
 	}
     }
-    virtual string startMatch() {return string("");}
-    virtual string endMatch() {return string("");}
+
+    virtual string startMatch(unsigned int)
+    {
+	return string("");
+    }
+
+    virtual string endMatch() 
+    {
+	return string("");
+    }
+
     const string &m_name;
 };
 
diff --git a/src/qtgui/preview_w.cpp b/src/qtgui/preview_w.cpp
index b0e8ab01..a0105175 100644
--- a/src/qtgui/preview_w.cpp
+++ b/src/qtgui/preview_w.cpp
@@ -68,54 +68,130 @@ using std::pair;
 // Subclass plainToRich to add s and anchors to the preview text
 class PlainToRichQtPreview : public PlainToRich {
 public:
-    int lastanchor;
-    PlainToRichQtPreview() 
+
+    PlainToRichQtPreview()
+	: m_curanchor(1), m_lastanchor(0)
     {
-	lastanchor = 0;
     }    
-    virtual ~PlainToRichQtPreview() {}
-    virtual string header() {
+
+    bool haveAnchors()
+    {
+	return m_lastanchor != 0;
+    }
+
+    virtual string header() 
+    {
 	if (m_inputhtml) {
-	    return snull;
+	    return cstr_null;
 	} else {
 	    if (prefs.previewPlainPre) {
 		m_eolbr = false;
 		return string(""
 			      "
");
-// Note we could also use the following for line-folding instead of 
s -// This would be possible without recomputing the whole text, much better perfs -// for toggling wrap/no-wrap -// "
");
+		// Note: we could also use the following for
+		// line-folding instead of 
s This would be + // possible without recomputing the whole text, much + // better perfs for toggling wrap/no-wrap: + //
 	    } else {
 		m_eolbr = true;
 		return string("");
 	    }
 	}
     }
-    virtual string startMatch() 
+
+    virtual string startMatch(unsigned int grpidx)
     {
-	return string("");
+	LOGDEB2(("startMatch, grpidx %u\n", grpidx));
+	grpidx = m_hdata->grpsugidx[grpidx];
+	LOGDEB2(("startMatch, ugrpidx %u\n", grpidx));
+	m_groupanchors[grpidx].push_back(++m_lastanchor);
+	m_groupcuranchors[grpidx] = 0; 
+	return string("").
+	    append("");
     }
-    virtual string endMatch() {return string("");}
-    virtual string termAnchorName(int i) {
+
+    virtual string endMatch() 
+    {
+	return string("");
+    }
+
+    virtual string termAnchorName(int i) const
+    {
 	static const char *termAnchorNameBase = "TRM";
 	char acname[sizeof(termAnchorNameBase) + 20];
 	sprintf(acname, "%s%d", termAnchorNameBase, i);
-	if (i > lastanchor)
-	    lastanchor = i;
 	return string(acname);
     }
 
-    virtual string startAnchor(int i) {
-	return string("";
+    virtual string startChunk() 
+    { 
+	return "
";
     }
-    virtual string endAnchor() {
-	return string("");
+
+    int nextAnchorNum(int grpidx)
+    {
+	LOGDEB2(("nextAnchorNum: group %d\n", grpidx));
+	map::iterator curit = 
+	    m_groupcuranchors.find(grpidx);
+	map >::iterator vecit = 
+	    m_groupanchors.find(grpidx);
+	if (grpidx == -1 || curit == m_groupcuranchors.end() ||
+	    vecit == m_groupanchors.end()) {
+	    if (m_curanchor >= m_lastanchor)
+		m_curanchor = 1;
+	    else
+		m_curanchor++;
+	} else {
+	    if (curit->second >= vecit->second.size() -1)
+		m_groupcuranchors[grpidx] = 0;
+	    else 
+		m_groupcuranchors[grpidx]++;
+	    m_curanchor = vecit->second[m_groupcuranchors[grpidx]];
+	    LOGDEB2(("nextAnchorNum: curanchor now %d\n", m_curanchor));
+	}
+	return m_curanchor;
     }
-    virtual string startChunk() { return "
";}
+
+    int prevAnchorNum(int grpidx)
+    {
+	map::iterator curit = 
+	    m_groupcuranchors.find(grpidx);
+	map >::iterator vecit = 
+	    m_groupanchors.find(grpidx);
+	if (grpidx == -1 || curit == m_groupcuranchors.end() ||
+	    vecit == m_groupanchors.end()) {
+	    if (m_curanchor <= 1)
+		m_curanchor = m_lastanchor;
+	    else
+		m_curanchor--;
+	} else {
+	    if (curit->second <= 0)
+		m_groupcuranchors[grpidx] = vecit->second.size() -1;
+	    else 
+		m_groupcuranchors[grpidx]--;
+	    m_curanchor = vecit->second[m_groupcuranchors[grpidx]];
+	}
+	return m_curanchor;
+    }
+
+    QString curAnchorName() const
+    {
+	return QString::fromUtf8(termAnchorName(m_curanchor).c_str());
+    }
+
+private:
+    int m_curanchor;
+    int m_lastanchor;
+    // Lists of anchor numbers (match locations) for the term (groups)
+    // in the query (the map key is and index into HighlightData.groups).
+    map > m_groupanchors;
+    map m_groupcuranchors;
 };
 
 void Preview::init()
@@ -141,8 +217,24 @@ void Preview::init()
     QHBoxLayout *layout3 = new QHBoxLayout(0); 
     searchLabel = new QLabel(this);
     layout3->addWidget(searchLabel);
-    searchTextLine = new QLineEdit(this);
-    layout3->addWidget(searchTextLine);
+
+    searchTextCMB = new QComboBox(this);
+    searchTextCMB->setEditable(true);
+    searchTextCMB->setInsertPolicy(QComboBox::NoInsert);
+    searchTextCMB->setDuplicatesEnabled(false);
+    for (unsigned int i = 0; i < m_hData.ugroups.size(); i++) {
+	QString s;
+	for (unsigned int j = 0; j < m_hData.ugroups[i].size(); j++) {
+	    s.append(QString::fromUtf8(m_hData.ugroups[i][j].c_str()));
+	    if (j != m_hData.ugroups[i].size()-1)
+		s.append(" ");
+	}
+	searchTextCMB->addItem(s);
+    }
+    searchTextCMB->setEditText("");
+
+    layout3->addWidget(searchTextCMB);
+
     nextButton = new QPushButton(this);
     nextButton->setEnabled(TRUE);
     layout3->addWidget(nextButton);
@@ -160,7 +252,7 @@ void Preview::init()
     resize(QSize(640, 480).expandedTo(minimumSizeHint()));
 
     // buddies
-    searchLabel->setBuddy(searchTextLine);
+    searchLabel->setBuddy(searchTextCMB);
 
     searchLabel->setText(tr("&Search for:"));
     nextButton->setText(tr("&Next"));
@@ -176,26 +268,25 @@ void Preview::init()
 			   "RCL.SEARCH.PREVIEW");
 
     // signals and slots connections
-    connect(searchTextLine, SIGNAL(textChanged(const QString&)), 
-	    this, SLOT(searchTextLine_textChanged(const QString&)));
+    connect(searchTextCMB, SIGNAL(activated(int)), 
+	    this, SLOT(searchTextFromIndex(int)));
+    connect(searchTextCMB, SIGNAL(editTextChanged(const QString&)), 
+	    this, SLOT(searchTextChanged(const QString&)));
     connect(nextButton, SIGNAL(clicked()), this, SLOT(nextPressed()));
     connect(prevButton, SIGNAL(clicked()), this, SLOT(prevPressed()));
-    connect(clearPB, SIGNAL(clicked()), searchTextLine, SLOT(clear()));
+    connect(clearPB, SIGNAL(clicked()), searchTextCMB, SLOT(clearEditText()));
     connect(pvTab, SIGNAL(currentChanged(QWidget *)), 
 	    this, SLOT(currentChanged(QWidget *)));
     connect(bt, SIGNAL(clicked()), this, SLOT(closeCurrentTab()));
 
     m_dynSearchActive = false;
     m_canBeep = true;
-    m_currentW = 0;
     if (prefs.pvwidth > 100) {
 	resize(prefs.pvwidth, prefs.pvheight);
     }
     m_loading = false;
     currentChanged(pvTab->currentWidget());
     m_justCreated = true;
-    m_haveAnchors = false;
-    m_curAnchor = 1;
 }
 
 void Preview::closeEvent(QCloseEvent *e)
@@ -273,11 +364,11 @@ bool Preview::eventFilter(QObject *target, QEvent *event)
     } else if (m_dynSearchActive) {
 	if (keyEvent->key() == Qt::Key_F3) {
 	    LOGDEB2(("Preview::eventFilter: got F3\n"));
-	    doSearch(searchTextLine->text(), true, false);
+	    doSearch(searchTextCMB->currentText(), true, false);
 	    return true;
 	}
-	if (target != searchTextLine)
-	    return QApplication::sendEvent(searchTextLine, event);
+	if (target != searchTextCMB)
+	    return QApplication::sendEvent(searchTextCMB, event);
     } else {
 	if (edit && 
 	    (target == edit || target == edit->viewport())) {
@@ -285,7 +376,7 @@ bool Preview::eventFilter(QObject *target, QEvent *event)
 		(keyEvent->key() == Qt::Key_F &&
 		 (keyEvent->modifiers() & Qt::ControlModifier))) {
 		LOGDEB2(("Preview::eventFilter: got / or C-F\n"));
-		searchTextLine->setFocus();
+		searchTextCMB->setFocus();
 		m_dynSearchActive = true;
 		return true;
 	    } else if (keyEvent->key() == Qt::Key_Space) {
@@ -307,23 +398,27 @@ bool Preview::eventFilter(QObject *target, QEvent *event)
     return false;
 }
 
-void Preview::searchTextLine_textChanged(const QString & text)
+void Preview::searchTextChanged(const QString & text)
 {
-    LOGDEB2(("search line text changed. text: '%s'\n", text.ascii()));
+    LOGDEB1(("Search line text changed. text: '%s'\n", 
+	     (const char *)text.toAscii()));
+    m_searchTextFromIndex = -1;
     if (text.isEmpty()) {
 	m_dynSearchActive = false;
-	//	nextButton->setEnabled(false);
-	//	prevButton->setEnabled(false);
 	clearPB->setEnabled(false);
     } else {
 	m_dynSearchActive = true;
-	//	nextButton->setEnabled(true);
-	//	prevButton->setEnabled(true);
 	clearPB->setEnabled(true);
 	doSearch(text, false, false);
     }
 }
 
+void Preview::searchTextFromIndex(int idx)
+{
+    LOGDEB1(("search line from index %d\n", idx));
+    m_searchTextFromIndex = idx;
+}
+
 PreviewTextEdit *Preview::currentEditor()
 {
     LOGDEB2(("Preview::currentEditor()\n"));
@@ -351,9 +446,9 @@ void Preview::emitSaveDocToFile()
 void Preview::doSearch(const QString &_text, bool next, bool reverse, 
 		       bool wordOnly)
 {
-    LOGDEB(("Preview::doSearch: text [%s] txtlen %d next %d rev %d word %d\n", 
-             (const char *)_text.toUtf8(), _text.length(), int(next), 
-             int(reverse), int(wordOnly)));
+    LOGDEB(("Preview::doSearch: text [%s] idx %d next %d rev %d word %d\n", 
+	    (const char *)_text.toUtf8(), m_searchTextFromIndex, int(next), 
+	    int(reverse), int(wordOnly)));
     QString text = _text;
 
     bool matchCase = matchCheck->isChecked();
@@ -363,25 +458,19 @@ void Preview::doSearch(const QString &_text, bool next, bool reverse,
 	return;
     }
 
-    if (text.isEmpty()) {
-	if (m_haveAnchors == false) {
+    if (text.isEmpty() || m_searchTextFromIndex != -1) {
+	if (!edit->m_plaintorich->haveAnchors()) {
 	    LOGDEB(("NO ANCHORS\n"));
 	    return;
 	}
+	// The combobox indices are equal to the search ugroup indices
+	// in hldata, that's how we built the list.
 	if (reverse) {
-	    if (m_curAnchor == 1)
-		m_curAnchor = edit->m_plaintorich->lastanchor;
-	    else
-		m_curAnchor--;
+	    edit->m_plaintorich->prevAnchorNum(m_searchTextFromIndex);
 	} else {
-	    if (m_curAnchor == edit->m_plaintorich->lastanchor)
-		m_curAnchor = 1;
-	    else
-		m_curAnchor++;
+	    edit->m_plaintorich->nextAnchorNum(m_searchTextFromIndex);
 	}
-	LOGDEB(("m_curAnchor: %d\n", m_curAnchor));
-	QString aname = 
-	   QString::fromUtf8(edit->m_plaintorich->termAnchorName(m_curAnchor).c_str());
+	QString aname = edit->m_plaintorich->curAnchorName();
 	LOGDEB(("Calling scrollToAnchor(%s)\n", (const char *)aname.toUtf8()));
 	edit->scrollToAnchor(aname);
 	// Position the cursor approximately at the anchor (top of
@@ -440,14 +529,14 @@ void Preview::doSearch(const QString &_text, bool next, bool reverse,
 
 void Preview::nextPressed()
 {
-    LOGDEB2(("PreviewTextEdit::nextPressed\n"));
-    doSearch(searchTextLine->text(), true, false);
+    LOGDEB2(("Preview::nextPressed\n"));
+    doSearch(searchTextCMB->currentText(), true, false);
 }
 
 void Preview::prevPressed()
 {
-    LOGDEB2(("PreviewTextEdit::prevPressed\n"));
-    doSearch(searchTextLine->text(), true, true);
+    LOGDEB2(("Preview::prevPressed\n"));
+    doSearch(searchTextCMB->currentText(), true, true);
 }
 
 // Called when user clicks on tab
@@ -456,7 +545,6 @@ void Preview::currentChanged(QWidget * tw)
     LOGDEB2(("PreviewTextEdit::currentChanged\n"));
     PreviewTextEdit *edit = 
 	tw->findChild("pvEdit");
-    m_currentW = tw;
     LOGDEB1(("Preview::currentChanged(). Editor: %p\n", edit));
     
     if (edit == 0) {
@@ -470,7 +558,7 @@ void Preview::currentChanged(QWidget * tw)
     connect(this, SIGNAL(printCurrentPreviewRequest()), edit, SLOT(print()));
     edit->installEventFilter(this);
     edit->viewport()->installEventFilter(this);
-    searchTextLine->installEventFilter(this);
+    searchTextCMB->installEventFilter(this);
     emit(previewExposed(this, m_searchId, edit->m_docnum));
 }
 
@@ -507,7 +595,7 @@ PreviewTextEdit *Preview::addEditorTab()
 
 void Preview::setCurTabProps(const Rcl::Doc &doc, int docnum)
 {
-    LOGDEB1(("PreviewTextEdit::setCurTabProps\n"));
+    LOGDEB1(("Preview::setCurTabProps\n"));
     QString title;
     string ctitle;
     if (doc.getmeta(Rcl::Doc::keytt, &ctitle) && !ctitle.empty()) {
@@ -721,8 +809,6 @@ bool Preview::loadDocInCurrentTab(const Rcl::Doc &idoc, int docnum)
     LoadGuard guard(&m_loading);
     CancelCheck::instance().setCancel(false);
 
-    m_haveAnchors = false;
-
     setCurTabProps(idoc, docnum);
 
     QString msg = QString("Loading: %1 (size %2 bytes)")
@@ -956,23 +1042,20 @@ bool Preview::loadDocInCurrentTab(const Rcl::Doc &idoc, int docnum)
 
 
     // Position the editor so that the first search term is visible
-    m_haveAnchors = editor->m_plaintorich->lastanchor != 0;
-    if (searchTextLine->text().length() != 0) {
+    if (searchTextCMB->currentText().length() != 0) {
 	// If there is a current search string, perform the search
 	m_canBeep = true;
-	doSearch(searchTextLine->text(), true, false);
+	doSearch(searchTextCMB->currentText(), true, false);
     } else {
 	// Position to the first query term
-	if (m_haveAnchors) {
-	    QString aname = 
-		QString::fromUtf8(editor->m_plaintorich->termAnchorName(1).c_str());
+	if (editor->m_plaintorich->haveAnchors()) {
+	    QString aname = editor->m_plaintorich->curAnchorName();
 	    LOGDEB2(("Call movetoanchor(%s)\n", (const char *)aname.toUtf8()));
 	    editor->scrollToAnchor(aname);
 	    // Position the cursor approximately at the anchor (top of
 	    // viewport) so that searches start from here
 	    QTextCursor cursor = editor->cursorForPosition(QPoint(0, 0));
 	    editor->setTextCursor(cursor);
-	    m_curAnchor = 1;
 	}
     }
 
@@ -989,14 +1072,15 @@ bool Preview::loadDocInCurrentTab(const Rcl::Doc &idoc, int docnum)
     return true;
 }
 
-PreviewTextEdit::PreviewTextEdit(QWidget* parent,const char* name, Preview *pv) 
-    : QTextEdit(parent), m_preview(pv), m_dspflds(false), m_docnum(-1) 
+PreviewTextEdit::PreviewTextEdit(QWidget* parent, const char* nm, Preview *pv) 
+    : QTextEdit(parent), m_preview(pv), 
+      m_plaintorich(new PlainToRichQtPreview()), 
+      m_dspflds(false), m_docnum(-1) 
 {
     setContextMenuPolicy(Qt::CustomContextMenu);
-    setObjectName(name);
+    setObjectName(nm);
     connect(this, SIGNAL(customContextMenuRequested(const QPoint&)),
 	    this, SLOT(createPopupMenu(const QPoint&)));
-    m_plaintorich = new PlainToRichQtPreview();
 }
 
 PreviewTextEdit::~PreviewTextEdit()
diff --git a/src/qtgui/preview_w.h b/src/qtgui/preview_w.h
index f9b1591b..ab479376 100644
--- a/src/qtgui/preview_w.h
+++ b/src/qtgui/preview_w.h
@@ -58,22 +58,27 @@ protected:
     void mouseDoubleClickEvent(QMouseEvent *);
 
 private:
-    PlainToRichQtPreview *m_plaintorich;
     Preview *m_preview;
-    bool     m_dspflds;
+    PlainToRichQtPreview *m_plaintorich;
+    
+    bool   m_dspflds;
     string m_url; // filename for this tab
     string m_ipath; // Internal doc path inside file
     int    m_docnum;  // Index of doc in db search results.
+
     // doc out of internfile (previous fields come from the index) with
     // main text erased (for space).
     Rcl::Doc m_fdoc; 
+
     // The input doc out of the index/query list
     Rcl::Doc m_dbdoc; 
+
     // Saved rich (or plain actually) text: the textedit seems to
     // sometimes (but not always) return its text stripped of tags, so
     // this is needed (for printing for example)
     QString  m_richtxt;
     Qt::TextFormat m_format;
+
     // Temporary file name (possibly, if displaying image). The
     // TempFile itself is kept inside main.cpp (because that's where
     // signal cleanup happens), but we use its name to ask for release
@@ -92,14 +97,14 @@ class Preview : public QWidget {
 
     Preview(int sid, // Search Id
 	    const HighlightData& hdata) // Search terms etc. for highlighting
-	: QWidget(0), m_searchId(sid), m_hData(hdata)
+	: QWidget(0), m_searchId(sid), m_searchTextFromIndex(-1), m_hData(hdata)
     {
 	init();
     }
-    ~Preview(){}
 
     virtual void closeEvent(QCloseEvent *e );
     virtual bool eventFilter(QObject *target, QEvent *event );
+
     /** 
      * Arrange for the document to be displayed either by exposing the tab 
      * if already loaded, or by creating a new tab and loading it.
@@ -112,7 +117,8 @@ class Preview : public QWidget {
     friend class PreviewTextEdit;
 
 public slots:
-    virtual void searchTextLine_textChanged(const QString& text);
+    virtual void searchTextChanged(const QString& text);
+    virtual void searchTextFromIndex(int);
     virtual void doSearch(const QString& str, bool next, bool reverse,
 			  bool wo = false);
     virtual void nextPressed();
@@ -138,18 +144,17 @@ private:
     int           m_searchId; 
 
     bool          m_dynSearchActive;
+    // Index value the search text comes from. -1 if text was edited
+    int           m_searchTextFromIndex;
+
     bool          m_canBeep;
     bool          m_loading;
-    QWidget      *m_currentW;
     HighlightData m_hData;
     bool          m_justCreated; // First tab create is different
-    bool          m_haveAnchors; // Search terms are marked in text
-    int           m_lastAnchor; // Number of last anchor. Then rewind to 1
-    int           m_curAnchor;
 
     QTabWidget* pvTab;
     QLabel* searchLabel;
-    QLineEdit* searchTextLine;
+    QComboBox *searchTextCMB;
     QPushButton* nextButton;
     QPushButton* prevButton;
     QPushButton* clearPB;
diff --git a/src/qtgui/reslist.cpp b/src/qtgui/reslist.cpp
index c3fc7ddb..491d93cf 100644
--- a/src/qtgui/reslist.cpp
+++ b/src/qtgui/reslist.cpp
@@ -250,12 +250,22 @@ string QtGuiResListPager::iconUrl(RclConfig *config, Rcl::Doc& doc)
 
 class PlainToRichQtReslist : public PlainToRich {
 public:
-    virtual ~PlainToRichQtReslist() {}
-    virtual string startMatch() {
+    virtual string startMatch(unsigned int idx)
+    {
+	if (m_hdata) {
+	    string s1, s2;
+	    stringsToString >(m_hdata->groups[idx], s1); 
+	    stringsToString >(m_hdata->ugroups[m_hdata->grpsugidx[idx]], s2);
+	    LOGDEB(("Reslist startmatch: group %s user group %s\n", s1.c_str(), s2.c_str()));
+	}
+		
 	return string("");
     }
-    virtual string endMatch() {return string("");}
+    virtual string endMatch() 
+    {
+	return string("");
+    }
 };
 static PlainToRichQtReslist g_hiliter;
 
diff --git a/src/qtgui/ssearch_w.h b/src/qtgui/ssearch_w.h
index f1156f42..16176c39 100644
--- a/src/qtgui/ssearch_w.h
+++ b/src/qtgui/ssearch_w.h
@@ -38,7 +38,6 @@ public:
 	setupUi(this);
 	init();
     }
-    ~SSearch(){}
 
     virtual void init();
     virtual void setAnyTermMode();
diff --git a/src/query/plaintorich.cpp b/src/query/plaintorich.cpp
index 57e32d18..8cb45ad7 100644
--- a/src/query/plaintorich.cpp
+++ b/src/query/plaintorich.cpp
@@ -49,13 +49,22 @@ static string vecStringToString(const vector& t)
     return sterms;
 }
 
+struct MatchEntry {
+    pair offs;
+    unsigned int grpidx;
+    MatchEntry(int sta, int sto, unsigned int idx) 
+	: offs(sta, sto), grpidx(idx)
+    {
+    }
+};
+
 // Text splitter used to take note of the position of query terms
 // inside the result text. This is then used to insert highlight tags.
 class TextSplitPTR : public TextSplit {
  public:
 
     // Out: begin and end byte positions of query terms/groups in text
-    vector > tboffs;  
+    vector tboffs;  
 
     TextSplitPTR(const HighlightData& hdata)
     :  m_wcount(0), m_hdata(hdata)
@@ -67,7 +76,7 @@ class TextSplitPTR : public TextSplit {
 	for (vector >::const_iterator vit = hdata.groups.begin();
 	     vit != hdata.groups.end(); vit++) {
 	    if (vit->size() == 1) {
-		m_terms.insert(vit->front());
+		m_terms[vit->front()] = vit - hdata.groups.begin();
 	    } else if (vit->size() > 1) {
 		for (vector::const_iterator it = vit->begin(); 
 		     it != vit->end(); it++) {
@@ -91,8 +100,9 @@ class TextSplitPTR : public TextSplit {
 	// pos, bts, bte));
 
 	// If this word is a search term, remember its byte-offset span. 
-	if (m_terms.find(dumb) != m_terms.end()) {
-	    tboffs.push_back(pair(bts, bte));
+	map::const_iterator it = m_terms.find(dumb);
+	if (it != m_terms.end()) {
+	    tboffs.push_back(MatchEntry(bts, bte, (*it).second));
 	}
 	
 	// If word is part of a search group, update its positions list
@@ -114,13 +124,13 @@ class TextSplitPTR : public TextSplit {
     virtual bool matchGroups();
 
 private:
-    virtual bool matchGroup(const vector& terms, int dist);
+    virtual bool matchGroup(unsigned int idx);
 
     // Word count. Used to call checkCancel from time to time.
     int m_wcount;
 
     // In: user query terms
-    set    m_terms; 
+    map    m_terms; 
 
     // m_gterms holds all the terms in m_groups, as a set for quick lookup
     set    m_gterms;
@@ -191,9 +201,12 @@ static bool do_proximity_test(int window, vector* >& plists,
     return false;
 }
 
-// Find NEAR matches for the input group of terms, update highlight map
-bool TextSplitPTR::matchGroup(const vector& terms, int window)
+// Find NEAR matches for one group of terms, update highlight map
+bool TextSplitPTR::matchGroup(unsigned int grpidx)
 {
+    const vector& terms = m_hdata.groups[grpidx];
+    int window = m_hdata.groups[grpidx].size() + m_hdata.slacks[grpidx];
+
     LOGDEB0(("TextSplitPTR::matchGroup:d %d: %s\n", window,
 	    vecStringToString(terms).c_str()));
 
@@ -203,26 +216,23 @@ bool TextSplitPTR::matchGroup(const vector& terms, int window)
     // A revert plist->term map. This is so that we can find who is who after
     // sorting the plists by length.
     map*, string> plistToTerm;
-    // For traces
-    vector realgroup;
 
-    // Find the position list for each term in the group. Not all
-    // necessarily exist (esp for NEAR where terms have been
-    // stem-expanded: we don't know which matched)
+    // Find the position list for each term in the group. It is
+    // possible that this particular group was not actually matched by
+    // the search, so that some terms are not found.
     for (vector::const_iterator it = terms.begin(); 
 	 it != terms.end(); it++) {
 	map >::iterator pl = m_plists.find(*it);
 	if (pl == m_plists.end()) {
 	    LOGDEB0(("TextSplitPTR::matchGroup: [%s] not found in m_plists\n",
 		    (*it).c_str()));
-	    continue;
+	    return false;
 	}
 	plists.push_back(&(pl->second));
 	plistToTerm[&(pl->second)] = *it;
-	realgroup.push_back(*it);
     }
-    LOGDEB0(("TextSplitPTR::matchGroup:d %d:real group after expansion %s\n", 
-	     window, vecStringToString(realgroup).c_str()));
+    // I think this can't actually happen, was useful when we used to
+    // prune the groups, but doesn't hurt.
     if (plists.size() < 2) {
 	LOGDEB0(("TextSplitPTR::matchGroup: no actual groups found\n"));
 	return false;
@@ -261,15 +271,13 @@ bool TextSplitPTR::matchGroup(const vector& terms, int window)
 	    SETMINMAX(pos, sta, sto);
 	    minpos = sto+1;
 	    // Translate the position window into a byte offset window
-	    int bs = 0;
 	    map >::iterator i1 =  m_gpostobytes.find(sta);
 	    map >::iterator i2 =  m_gpostobytes.find(sto);
 	    if (i1 != m_gpostobytes.end() && i2 != m_gpostobytes.end()) {
 		LOGDEB0(("TextSplitPTR::matchGroup: pushing bpos %d %d\n",
 			i1->second.first, i2->second.second));
-		tboffs.push_back(pair(i1->second.first, 
-						i2->second.second));
-		bs = i1->second.first;
+		tboffs.push_back(MatchEntry(i1->second.first, 
+					    i2->second.second, grpidx));
 	    } else {
 		LOGDEB(("matchGroup: no bpos found for %d or %d\n", sta, sto));
 	    }
@@ -284,22 +292,23 @@ bool TextSplitPTR::matchGroup(const vector& terms, int window)
 /** Sort integer pairs by increasing first value and decreasing width */
 class PairIntCmpFirst {
 public:
-    bool operator()(pair a, pairb) {
-	if (a.first != b.first)
-	    return a.first < b.first;
-	return a.second > b.second;
+    bool operator()(const MatchEntry& a, const MatchEntry& b) {
+	if (a.offs.first != b.offs.first)
+	    return a.offs.first < b.offs.first;
+	return a.offs.second > b.offs.second;
     }
 };
 
-// Look for matches to PHRASE and NEAR term groups. Actually, we
-// handle all groups as NEAR (ignore order).
+// Look for matches to PHRASE and NEAR term groups and finalize the
+// matched regions list (sort it by increasing start then decreasing
+// length)
+// Actually, we handle all groups as NEAR (ignore order).
 bool TextSplitPTR::matchGroups()
 {
     for (unsigned int i = 0; i < m_hdata.groups.size(); i++) {
 	if (m_hdata.groups[i].size() <= 1)
 	    continue;
-	matchGroup(m_hdata.groups[i], 
-		   m_hdata.groups[i].size() + m_hdata.slacks[i]);
+	matchGroup(i);
     }
 
     // Sort regions by increasing start and decreasing width.  
@@ -324,6 +333,7 @@ bool PlainToRich::plaintorich(const string& in,
 {
     Chrono chron;
 
+    m_hdata = &hdata;
     // Compute the positions for the query terms.  We use the text
     // splitter to break the text into words, and compare the words to
     // the search terms,
@@ -346,8 +356,8 @@ bool PlainToRich::plaintorich(const string& in,
     // Iterator for the list of input term positions. We use it to
     // output highlight tags and to compute term positions in the
     // output text
-    vector >::iterator tPosIt = splitter.tboffs.begin();
-    vector >::iterator tPosEnd = splitter.tboffs.end();
+    vector::iterator tPosIt = splitter.tboffs.begin();
+    vector::iterator tPosEnd = splitter.tboffs.end();
 
 #if 0
     for (vector >::const_iterator it = splitter.tboffs.begin();
@@ -365,8 +375,6 @@ bool PlainToRich::plaintorich(const string& in,
     int hadcr = 0;
     int inindent = 1;
 
-    // Value for numbered anchors at each term match
-    int anchoridx = 1;
     // HTML state
     bool intag = false, inparamvalue = false;
     // My tag state
@@ -391,22 +399,20 @@ bool PlainToRich::plaintorich(const string& in,
 	// we are at or after a term match, mark.
 	if (tPosIt != tPosEnd) {
 	    int ibyteidx = chariter.getBpos();
-	    if (ibyteidx == tPosIt->first) {
+	    if (ibyteidx == tPosIt->offs.first) {
 		if (!intag && ibyteidx >= (int)headend) {
-		    *olit += startAnchor(anchoridx);
-		    *olit += startMatch();
+		    *olit += startMatch(tPosIt->grpidx);
 		}
-		anchoridx++;
                 inrcltag = 1;
-	    } else if (ibyteidx == tPosIt->second) {
+	    } else if (ibyteidx == tPosIt->offs.second) {
 		// Output end of match region tags
 		if (!intag && ibyteidx > (int)headend) {
 		    *olit += endMatch();
-		    *olit += endAnchor();
 		}
 		// Skip all highlight areas that would overlap this one
-		int crend = tPosIt->second;
-		while (tPosIt != splitter.tboffs.end() && tPosIt->first < crend)
+		int crend = tPosIt->offs.second;
+		while (tPosIt != splitter.tboffs.end() && 
+		       tPosIt->offs.first < crend)
 		    tPosIt++;
                 inrcltag = 0;
 	    }
diff --git a/src/query/plaintorich.h b/src/query/plaintorich.h
index 21ddbc98..cbb13d9e 100644
--- a/src/query/plaintorich.h
+++ b/src/query/plaintorich.h
@@ -21,6 +21,7 @@
 #include 
 
 #include "hldata.h"
+#include "cstr.h"
 
 /** 
  * A class for highlighting search results. Overridable methods allow
@@ -31,7 +32,7 @@
 class PlainToRich {
 public:
     PlainToRich() 
-	: m_inputhtml(false) 
+	: m_inputhtml(false), m_eolbr(false), m_hdata(0)
     {
     }
 
@@ -71,18 +72,35 @@ public:
 			     );
 
     /* Overridable output methods for headers, highlighting and marking tags */
-    virtual std::string header() {return snull;}
-    virtual std::string startMatch() {return snull;}
-    virtual std::string endMatch() {return snull;}
-    virtual std::string startAnchor(int) {return snull;}
-    virtual std::string endAnchor() {return snull;}
-    virtual std::string startChunk() {return snull;}
+
+    virtual std::string header() 
+    {
+	return cstr_null;
+    }
+
+    /** Return match prefix (e.g.: 
). + @param groupidx the index into hdata.groups */ + virtual std::string startMatch(unsigned int) + { + return cstr_null; + } + + /** Return data for end of match area (e.g.:
). */ + virtual std::string endMatch() + { + return cstr_null; + } + + virtual std::string startChunk() + { + return cstr_null; + } protected: - const std::string snull; bool m_inputhtml; // Use
to break plain text lines (else caller has used a
 tag)
     bool m_eolbr; 
+    const HighlightData *m_hdata;
 };
 
 #endif /* _PLAINTORICH_H_INCLUDED_ */
diff --git a/src/query/reslistpager.cpp b/src/query/reslistpager.cpp
index 7027dd94..9718709f 100644
--- a/src/query/reslistpager.cpp
+++ b/src/query/reslistpager.cpp
@@ -40,9 +40,14 @@ static const string cstr_hlfontcolor("");
 static const string cstr_hlendfont("");
 class PlainToRichHtReslist : public PlainToRich {
 public:
-    virtual ~PlainToRichHtReslist() {}
-    virtual string startMatch() {return cstr_hlfontcolor;}
-    virtual string endMatch() {return cstr_hlendfont;}
+    virtual string startMatch(unsigned int) 
+    {
+	return cstr_hlfontcolor;
+    }
+    virtual string endMatch() 
+    {
+	return cstr_hlendfont;
+    }
 };
 static PlainToRichHtReslist g_hiliter;