#include "Collectiondb.h"
#include "HttpServer.h"
#include "Stats.h"
#include "Statistics.h"
#include "Query.h"
#include "Speller.h"
#include "Msg40.h"
#include "Pages.h"
#include "Highlight.h"
#include "SearchInput.h"
#include <math.h>
#include "SafeBuf.h"
#include "Pos.h"
#include "Bits.h"
#include "sort.h"
#include "CountryCode.h"
#include "Posdb.h"
#include "PosdbTable.h"
#include "PageResults.h"
#include "PageRoot.h"
#include "Proxy.h"
#include "Json.h"
#include "Images.h" //Thumbnail*
#include "HttpMime.h"
#include "Process.h"
#include "Linkdb.h"
#include "XmlDoc.h"
#include "ip.h"
#include "GbUtil.h"
#include "Conf.h"
#include "Mem.h"
#include "RobotsBlockedResultOverride.h"
#include "QueryLanguage.h"
#include "FxLanguage.h"
#include "Errno.h"
#ifdef _VALGRIND_
#include <valgrind/memcheck.h>
#endif


static bool printSearchResultsHeader(State0 *st);
static bool printResult(State0 *st, int32_t ix, int32_t *numPrintedSoFar);
static bool printSearchResultsTail(State0 *st);
static bool printSearchFiltersBar ( SafeBuf *sb , HttpRequest *hr ) ;
static bool printMenu ( SafeBuf *sb , int32_t menuNum , HttpRequest *hr ) ;

static void gotQueryLanguageWrapper(void *state, const std::vector<std::pair<lang_t, double>> &languages);
static bool gotQueryLanguage(State0 *st, const std::vector<std::pair<lang_t, double>> &languages);

static void gotResultsWrapper  ( void *state ) ;
static bool gotResults         ( void *state ) ;

static bool replaceParm ( const char *cgi , SafeBuf *newUrl , HttpRequest *hr ) ;
static bool replaceParm2 ( const char *cgi , SafeBuf *newUrl ,
			   const char *oldUrl , int32_t oldUrlLen ) ;


static bool printPairScore(SafeBuf *sb, const SearchInput *si, const PairScore *ps, Msg20Reply *mr);

static bool printScoresHeader ( SafeBuf *sb ) ;

static bool printMetaContent ( Msg40 *msg40 , int32_t i ,State0 *st, SafeBuf *sb );

static bool printSingleScore(SafeBuf *sb, const SearchInput *si, const SingleScore *ss, Msg20Reply *mr);

static bool printSingleTerm(SafeBuf *sb, const Query *q, const SingleScore *ss);
static bool printTermPairs(SafeBuf *sb, const Query *q, const PairScore *ps);

static bool printLogoAndSearchBox (SafeBuf *sb , class HttpRequest *hr, const SearchInput *si );

State0::State0()
	: m_sb()
	, m_header(true)
	, m_collnum(-1)
	, m_si()
	, m_msg40()
	, m_socket(nullptr)
	, m_startTime(0)
	, m_gotResults(false)
	, m_errno(0)
	, m_numDocIds(0)
	, m_took(0)
	, m_hr()
	, m_qesb()
	, m_xd(nullptr)
	, m_socketStartTimeHack(0)
	, m_primaryQueryLanguage(langUnknown) {
}

static bool sendReply(State0 *st, const char *reply, int32_t rlen) {

	int32_t savedErr = g_errno;

	TcpSocket *sock = st->m_socket;
	if ( ! sock ) { 
		log("results: not sending back results on an empty socket."
		    "socket must have closed on us abruptly.");
		//g_process.shutdownAbort(true); }
	}
	SearchInput *si = &st->m_si;
	const char *ct = "text/html";
	if ( si->m_format == FORMAT_XML ) ct = "text/xml"; 
	if ( si->m_format == FORMAT_JSON ) ct = "application/json";

	const char *charset = "utf-8";
	char format = si->m_format;

	// . filter anything < 0x20 to 0x20 to keep XML legal
	// . except \t, \n and \r, they're ok
	// . gotta set "f" down here in case it realloc'd the buf
	if ( format == FORMAT_XML && reply ) {
		unsigned char *f = (unsigned char *)reply;
		for ( ; *f ; f++ ) 
			if ( *f < 0x20 && *f!='\t' && *f!='\n' && *f!='\r' ) 
				*f = 0x20;
	}


	logf(LOG_DEBUG,"gb: sending back %" PRId32" bytes",rlen);

	Statistics::register_query_time(si->m_q.m_numWords, si->m_queryLangId, savedErr, (gettimeofdayInMilliseconds() - st->m_startTime));

	// . log the time
	// . do not do this if g_errno is set lest m_sbuf1 be bogus b/c
	//   it failed to allocate its buf to hold terminating \0 in
	//   SearchInput::setQueryBuffers()
	if ( ! g_errno && st->m_took >= g_conf.m_logQueryTimeThreshold ) {
		logf(LOG_WARN,"Query took %" PRId64" ms for %s. results=%" PRId32,
		     st->m_took,
		     si->m_sbuf1.getBufStart(),
		     st->m_msg40.getNumResults());
	}

	if ( ! savedErr ) {
		// . one hour cache time... no 1000 hours, basically infinite
		// . no because if we redo the query the results are cached
		//int32_t cacheTime = 3600;//*1000;
		// no... do not use cache
		int32_t cacheTime = -1;
		// the "Check it" link on add url uses &usecache=0 to tell
		// the browser not to use its cache...
		//if ( hr->getLong("usecache",-1) == 0 ) cacheTime = 0;
		//
		// send back the actual search results
		//
		if ( sock )
		g_httpServer.sendDynamicPage(sock,
					     reply,
					     rlen,//strlen(reply),
					     // don't let the ajax re-gen
					     // if they hit the back button!
					     // so make this 1 hour, not 0
					     cacheTime, // cachetime in secs
					     false, // POSTReply?
					     ct,
					     -1, // httpstatus -1 -> 200
					     NULL, // cookieptr
					     charset );

		// free st after sending reply since "st->m_sb" = "reply"
		mdelete(st, sizeof(State0), "PageResults2");
		delete st;
		return true;
	}

	mdelete(st, sizeof(State0), "PageResults2");
	delete st;

	// if we had a broken pipe from the browser while sending
	// them the search results, then we end up closing the socket fd
	// in TcpServer::sendChunk() > sendMsg() > destroySocket()
	if ( sock && sock->m_numDestroys ) {
		log("results: not sending back error on destroyed socket "
		    "sd=%" PRId32,sock->m_sd);
		return true;
	}

	int32_t status = 500;
	if (savedErr == EBADREQUEST ||
	    savedErr == ENOPERM ||
	    savedErr == ENOCOLLREC) 
		status = 400;

	if ( sock )
	g_httpServer.sendQueryErrorReply(sock,
					 status,
					 mstrerror(savedErr),
					 format,//xml,
					 savedErr, 
					 "There was an error!");
	return true;
}

static bool printCSSHead(SafeBuf *sb) {
	sb->safePrintf(
			      "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML "
			      "4.01 Transitional//EN\">\n"
			      //"<meta http-equiv=\"Content-Type\" "
			      //"content=\"text/html; charset=utf-8\">\n"
			      "<html>\n"
			      "<head>\n"
			      "<title>Gigablast Search Results</title>\n"
			      "<style><!--"
			      "body {"
			      "font-family:Arial, Helvetica, sans-serif;"
			      );

	sb->safePrintf(	      "color: #000000;"
			      "font-size: 12px;"
			      //"margin: 20px 5px;"
			      "}"
			      "a:link {color:#00c}"
			      "a:visited {color:#551a8b}"
			      "a:active {color:#f00}"
			      ".bold {font-weight: bold;}"
			      ".bluetable {background:#d1e1ff;"
			      "margin-bottom:15px;font-size:12px;}"
			      ".url {color:#008000;}"
			      ".cached, .cached a {font-size: 10px;"
			      "color: #666666;"
			      "}"
			      "table {"
			      "font-family:Arial, Helvetica, sans-serif;"
			      "color: #000000;"
			      "font-size: 12px;"
			      "}"
			      ".directory {font-size: 16px;}"
			      "-->\n"
			      "</style>\n"
			      "</head>\n"
			      );
	return true;
}

// . returns false if blocked, true otherwise
// . sets g_errno on error
// . "msg" will be inserted into the access log for this request
bool sendPageResults ( TcpSocket *s , HttpRequest *hr ) {
	// make a new state
	State0 *st;
	try {
		st = new State0();
	} catch(std::bad_alloc&) {
		g_errno = ENOMEM;
		log(LOG_ERROR, "query: Query failed. "
		    "Could not allocate %" PRId32" bytes for query. "
		    "Returning HTTP status of 500.",(int32_t)sizeof(State0));
		Statistics::register_query_time(0, langUnknown, g_errno, 0);

		return g_httpServer.sendQueryErrorReply(s, 500, mstrerror(g_errno), hr->getReplyFormat(), g_errno,
		                                        "Query failed. Could not allocate memory to execute a search. Please try later.");
	}

	mnew(st, sizeof(State0), "PageResults2");

	// copy yhits
	if (!st->m_hr.copy(hr)) {
		return sendReply(st,nullptr,0);
	}

	// set this in case SearchInput::set fails!
	st->m_socket = s;

	// record timestamp so we know if we got our socket closed and swapped
	st->m_socketStartTimeHack = s->m_startTime;

	// you have to say "&header=1" to get back the header for json now.
	// later on maybe it will default to on.
	st->m_header = hr->getLong("header", 1);

	// get query parameters
	int32_t queryLen = 0;
	const char *query = hr->getString("q", &queryLen, "");

	const char *fx_qlang = hr->getString("fx_qlang", nullptr, "");
	const char *fx_blang = hr->getString("fx_blang", nullptr, "");
	const char *fx_country = hr->getString("fx_country", nullptr, "");
	const char *fx_fetld = hr->getString("fx_fetld", nullptr, "");

	
	// First try our built-in langauge detection which uses CLD2 and the top-quality accept-language item
	std::string contentLanguage;
	if (strlen(fx_qlang) && getLangIdFromAbbr(fx_qlang) != langUnknown) {
		contentLanguage = fx_qlang;
	} else if (strlen(fx_blang)) {
		// en-GB,en;q=0.8,fr;q=0.6,en-US;q=0.4
		std::string blang_hint(fx_blang);

		std::vector<std::pair<std::string, double>> blang_quality_pairs;
		// split between entries
		auto items = split(blang_hint, ',');
		for (const auto &item : items) {
			// en-US;q=0.4
			auto pairs = split(item, ';');
			double q = 1;
			if (pairs.size() == 2) {
				// has language quality factor
				auto nvp = split(pairs[1], '=');
				if (nvp.size() == 2) {
					q = strtod(nvp[1].c_str(), nullptr);
				}
			}

			// en-US
			auto tokens = split(pairs[0], '-');

			blang_quality_pairs.emplace_back(pairs[0], q);
		}

		std::sort(blang_quality_pairs.begin(), blang_quality_pairs.end(),
		          [](const std::pair<std::string, double> &a, const std::pair<std::string, double> &b) {
			          return a.second > b.second;
		          });

		auto it = std::unique(blang_quality_pairs.begin(), blang_quality_pairs.end(),
		                      [](const std::pair<std::string, double> &a, const std::pair<std::string, double> &b) {
			                      return a.first == b.first;
		                      });
		blang_quality_pairs.erase(it, blang_quality_pairs.end());

		if (blang_quality_pairs.size()) {
			double max_weight = blang_quality_pairs[0].second;
			for (auto blang_quality_pair : blang_quality_pairs) {
				if (!contentLanguage.empty()) {
					contentLanguage.append(",");
				}

				// append all blang with max weight
				if (blang_quality_pair.second == max_weight) {
					contentLanguage.append(blang_quality_pair.first);
					continue;
				}

				break;
			}
		}
	}

	const char *tld_hint = fx_fetld;
	if (!tld_hint || strlen(tld_hint) == 0 || strlen(tld_hint) != 2) {
		tld_hint = fx_country;
	}

	st->m_primaryQueryLanguage = FxLanguage::getLangIdCLD2(true, query, queryLen, contentLanguage.c_str(), contentLanguage.size(),
	                                                       tld_hint, strlen(tld_hint), true);


	//We can't use st->m_si.m_baseScoringParameters.allLanguageWeightsAreTheSame() because the cgi parameters have not set the members in SearchInput yet.
	//So we have to check if any CGI parameter using the "lw_" prefix has been specified.
	bool any_specific_language_weight_specified = false;
	for(int32_t i = 0; i < hr->getNumFields(); i++) {
		if(!hr->getValue(i))
			continue;
		const char *full_field_name = hr->getField(i);
		if(strncmp(full_field_name,"lw_",3)==0) {
			any_specific_language_weight_specified = true;
			break;
		}
	}
	
	if(!any_specific_language_weight_specified) {
		// Then try the external language detection server
		if (g_queryLanguage.getLanguage(st, gotQueryLanguageWrapper, fx_qlang, fx_blang, fx_country, fx_fetld, query)) {
			// blocked
			return false;
		}
	} else
		log(LOG_DEBUG,"Explicit language weight specified. Not contacting external language detection server");
	return gotQueryLanguage(st, {});
}

static void gotQueryLanguageWrapper(void *state, const std::vector<std::pair<lang_t, double>> &language_weights) {
	State0 *st = reinterpret_cast<State0 *>(state);
	if (!language_weights.empty()) {
		st->m_primaryQueryLanguage = language_weights.front().first;
	}

	gotQueryLanguage(st,language_weights);
}

static bool gotQueryLanguage(State0 *st, const std::vector<std::pair<lang_t, double>> &language_weights) {
	// . parse it up
	// . this returns false and sets g_errno and, maybe, g_msg on error
	SearchInput *si = &st->m_si;

	// si just copies the ptr into the httprequest
	// into stuff like SearchInput::m_defaultSortLanguage
	// so do not use the "hr" on the stack. SearchInput::
	// m_hr points to the hr we pass into
	// SearchInput::set
	if (!si->set(st->m_socket, &st->m_hr, st->m_primaryQueryLanguage, language_weights)) {
		log("query: set search input: %s",mstrerror(g_errno));
		if ( ! g_errno ) g_errno = EBADENGINEER;
		return sendReply(st,NULL,0);
	}

	// save collnum now
	st->m_collnum = si->m_cr ? si->m_cr->m_collnum : -1;

	st->m_numDocIds = si->m_docsWanted;

	// add query stat
	st->m_startTime = gettimeofdayInMilliseconds();

	// debug msg
	log(LOG_DEBUG, "query: Getting search results for q=%s", st->m_si.m_displayQuery);

	// for now disable queries
	if (!g_conf.m_queryingEnabled) {
		g_errno = EQUERYINGDISABLED;
		return sendReply(st, nullptr,0);
	}

	// LAUNCH RESULTS

	// . get some results from it
	// . this returns false if blocked, true otherwise
	// . it also sets g_errno on error
	// . use a niceness of 0 for all queries so they take precedence
	//   over the indexing process
	// . this will copy our passed "query" and "coll" to it's own buffer
	// . we print out matching docIds to int32_t if m_isDebug is true
	// . no longer forward this, since proxy will take care of evenly
	//   distributing its msg 0xfd "forward" requests now
	st->m_gotResults = st->m_msg40.getResults(si, false, st, gotResultsWrapper);

	// save error
	st->m_errno = g_errno;

	// wait for results?
	if (!st->m_gotResults) {
		return false;
	}

	// otherwise call gotResults which returns false if blocked, true else
	// and sets g_errno on error
	return gotResults(st);
}

static void gotResultsWrapper ( void *state ) {
	// cast our State0 class from this
	State0 *st = (State0 *) state;

	// save error
	st->m_errno = g_errno;

	// mark as gotten
	st->m_gotResults = true;

	gotResults(st);
}

// . make a web page from results stored in msg40
// . send it on TcpSocket "s" when done
// . returns false if blocked, true otherwise
// . sets g_errno on error
static bool gotResults ( void *state ) {
	// cast our State0 class from this
	State0 *st = (State0 *) state;

	int64_t nowMS = gettimeofdayInMilliseconds();
	// log the time
	int64_t took = nowMS - st->m_startTime;
	// record that
	st->m_took = took;

	//log("results: debug: in gotResults state=%" PTRFMT,(PTRTYPE)st);

	// grab the query
	Msg40 *msg40 = &(st->m_msg40);
	//char  *q    = msg40->getQuery();
	//int32_t   qlen = msg40->getQueryLen();

	SearchInput *si = &st->m_si;

	// if we lost the socket because we were streaming and it
	// got closed from a broken pipe or something, then Msg40.cpp
	// will set st->m_socket to NULL if the fd ends up ending closed
	// because someone else might be using it and we do not want to
	// mess with their TcpSocket settings.
	if ( ! st->m_socket ) {
		log("results: socket is NULL. sending failed.");
		return sendReply(st,NULL,0);
	}

	// if we skipped a shard because it was dead, usually we provide
	// the results anyway, but if this switch is true then return an
	// error code instead. this is the 'all or nothing' switch.
	if ( msg40->m_msg3a.m_skippedShards > 0 &&
	     ! g_conf.m_returnResultsAnyway ) {
	       char reply[256];
	       sprintf ( reply , 
			 "%" PRId32" shard(s) out of %" PRId32" did not "
			 "respond to query."
			 , msg40->m_msg3a.m_skippedShards
			 , g_hostdb.m_numShards );
	       g_errno = ESHARDDOWN;
	       return sendReply(st,reply,strlen(reply));
	}


	// collection rec must still be there since SearchInput references 
	// into it, and it must be the SAME ptr too!
	CollectionRec *cr = si->m_cr;//g_collectiondb.getRec ( collnum );
	if ( ! cr ) { // || cr != si->m_cr ) {
		g_errno = ENOCOLLREC;
		return sendReply(st,NULL,0);
	}

	// this causes ooms everywhere, not a good fix
	if ( ! msg40->m_msg20 && ! si->m_docIdsOnly && msg40->m_errno ) {
		log("msg40: failed to get results q=%s",si->m_q.originalQuery());
	 	//g_errno = ENOMEM;
		g_errno = msg40->m_errno;
		return sendReply(st,NULL,0);
	}


 	int32_t numResults = msg40->getNumResults();

	SafeBuf *sb = &st->m_sb;

	// print logo, search box, results x-y, ... into st->m_sb
	printSearchResultsHeader ( st );

	// then print each result
	// don't display more than docsWanted results
	int32_t count = msg40->getDocsWanted();
	bool hadPrintError = false;
	int32_t numPrintedSoFar = 0;

	for ( int32_t i = 0 ; count > 0 && i < numResults ; i++ ) {
		//////////
		//
		// prints in xml or html
		//
		//////////
		if ( ! printResult ( st , i , &numPrintedSoFar ) ) {
			hadPrintError = true;
			break;
		}

		// limit it
		count--;
	}


	if ( hadPrintError ) {
		if ( ! g_errno ) g_errno = EBADENGINEER;
		log("query: had error: %s",mstrerror(g_errno));
	}

	// wrap it up with Next 10 etc.
	printSearchResultsTail ( st );

	// END SERP DIV
	if ( si->m_format == FORMAT_WIDGET_IFRAME ||
	     si->m_format == FORMAT_WIDGET_AJAX )
		sb->safePrintf("</div>");

	// send it off
	sendReply(st, st->m_sb.getBufStart(), st->m_sb.length());

	return true;
}


static bool printLeftNavColumn ( SafeBuf &sb, State0 *st ) {

	SearchInput *si = &st->m_si;
	CollectionRec *cr = si->m_cr;

	char format = si->m_format;

	if ( format == FORMAT_HTML ) {
		const char *title = "Search Results";
		sb.safePrintf("<title>Gigablast - %s</title>\n",title);
		sb.safePrintf("<style><!--\n");
		sb.safePrintf("body {\n");
		sb.safePrintf("font-family:Arial, Helvetica, sans-serif;\n");
		sb.safePrintf("color: #000000;\n");
		sb.safePrintf("font-size: 12px;\n");
		sb.safePrintf("margin: 0px 0px;\n");
		sb.safePrintf("letter-spacing: 0.04em;\n");
		sb.safePrintf("}\n");
		sb.safePrintf("a {text-decoration:none;}\n");
		sb.safePrintf(".bold {font-weight: bold;}\n");
		sb.safePrintf(".bluetable {background:#d1e1ff;"
			      "margin-bottom:15px;font-size:12px;}\n");
		sb.safePrintf(".url {color:#008000;}\n");
		sb.safePrintf(".cached, .cached a {font-size: 10px;"
			      "color: #666666;\n");
		sb.safePrintf("}\n");
		sb.safePrintf("table {\n");
		sb.safePrintf("font-family:Arial, Helvetica, sans-serif;\n");
		sb.safePrintf("color: #000000;\n");
		sb.safePrintf("font-size: 12px;\n");
		sb.safePrintf("}\n");
		sb.safePrintf(".directory {font-size: 16px;}\n"
			      ".nav {font-size:20px;align:right;}\n"
			      );
		sb.safePrintf("-->\n");
		sb.safePrintf("</style>\n");
		sb.safePrintf("\n");
		sb.safePrintf("</head>\n");
		sb.safePrintf("<script>\n");
		sb.safePrintf("<!--\n");
		sb.safePrintf("var openmenu=''; var inmenuclick=0;");
		sb.safePrintf("function x(){document.f.q.focus();}\n");
		sb.safePrintf("// --></script>\n");
		sb.safePrintf("<body "

			      "onmousedown=\""

			      "if (openmenu != '' && inmenuclick==0) {"
			      "document.getElementById(openmenu)."
			      "style.display='none'; openmenu='';"
			      "}"

			      "inmenuclick=0;"
			      "\" "

			      "onload=\"x()\">\n");

		//
		// DIVIDE INTO TWO PANES, LEFT COLUMN and MAIN COLUMN
		//
		sb.safePrintf("<TABLE border=0 height=100%% cellpadding=0 "
			      "cellspacing=0>"
			      "\n<TR>\n");

		//
		// first the nav column
		//

		// . also prints <TD>...</TD>. true=isSearchresults
		// . tabName = "search"
		printLeftColumnRocketAndTabs ( &sb , true , cr , "search" );

		//
		// now the MAIN column
		//
		sb.safePrintf("\n</TD>"
			      "<TD valign=top style=padding-left:30px;>\n");
	}

	return true;
}

static bool printIgnoredWords ( SafeBuf *sb , const SearchInput *si ) {
	// mention ignored query terms
	// we need to set another Query with "keepAllSingles" set to false
	const Query *qq2 = &si->m_q;
	bool firstIgnored = true;
	for ( int32_t i = 0 ; i < qq2->m_numWords ; i++ ) {
		//if ( si->m_xml ) break;
		const QueryWord *qw = &qq2->m_qwords[i];
		// only print out words ignored cuz they were stop words
		if ( qw->m_ignoreWord != IGNORE_QSTOP ) continue;
		// print header -- we got one
		if ( firstIgnored ) {
			if ( si->m_format == FORMAT_XML )
				sb->safePrintf ("\t<ignoredWords><![CDATA[");
			else if ( si->m_format == FORMAT_JSON )
				sb->safePrintf ("\t\"ignoredWords\":\"");
			else if ( si->m_format == FORMAT_HTML )
				sb->safePrintf ("<br><font "
					       "color=\"#707070\">The "
					       "following query words "
					       "were ignored: "
					       "<b>");
			firstIgnored = false;
		}
		// print the word
		sb->utf8Encode2(qw->m_word, qw->m_wordLen);
		sb->safePrintf (" ");
	}
	// print tail if we had ignored terms
	if ( ! firstIgnored ) {
		sb->incrementLength(-1);
		if ( si->m_format == FORMAT_XML )
			sb->safePrintf("]]></ignoredWords>\n");
		else if ( si->m_format == FORMAT_JSON )
			sb->safePrintf("\",\n");
		else if ( si->m_format == FORMAT_HTML )
			sb->safePrintf ("</b>. Preceed each with a '+' or "
				       "wrap in "
				       "quotes to not ignore.</font>");
	}
	return true;
}

static bool printSearchResultsHeader(State0 *st) {

	const SearchInput *si = &st->m_si;

	// grab the query
	Msg40 *msg40 = &(st->m_msg40);
	const char *q    = msg40->getQuery();
	int32_t     qlen = msg40->getQueryLen();

  	//char  local[ 128000 ];
	//SafeBuf sb(local, 128000);
	SafeBuf *sb = &st->m_sb;
	// reserve 1.5MB now!
	if ( ! sb->reserve(1500000 ,"pgresbuf" ) ) // 128000) )
		return false;
	// just in case it is empty, make it null terminated
	sb->nullTerm();

	// print first [ for json
	if ( si->m_format == FORMAT_JSON ) {
		if ( st->m_header ) sb->safePrintf("{\n");
		// this is just for diffbot really...
		else                sb->safePrintf("[\n");
	}

	CollectionRec *cr = si->m_cr;
	HttpRequest *hr = &st->m_hr;

	// if there's a ton of sites use the post method otherwise
	// they won't fit into the http request, the browser will reject
	// sending such a large request with "GET"
	const char *method = "GET";
	if ( si->m_sites && strlen(si->m_sites)>800 ) method = "POST";


	if ( si->m_format == FORMAT_HTML &&
	     cr->m_htmlHead.length() ) {
		return expandHtml ( *sb ,
				    cr->m_htmlHead.getBufStart(),
				    cr->m_htmlHead.length(),
				    q,
				    qlen,
				    hr,
				    si,
				    method,
				    cr);
	}
				 

	if ( si->m_format == FORMAT_HTML ) {
		printCSSHead (sb);
		sb->safePrintf("<body>");
	}

	if ( si->m_format==FORMAT_WIDGET_IFRAME ) {
		printCSSHead(sb);
		sb->safePrintf("<body style=padding:0px;margin:0px;>");

		int32_t refresh = hr->getLong("refresh",0);
		if ( refresh )
			sb->safePrintf("<meta http-equiv=\"refresh\" "
				       "content=%" PRId32">",refresh);
	}

	// lead with user's widget header which usually has custom style tags
	if ( si->m_format == FORMAT_WIDGET_IFRAME ||
	     si->m_format == FORMAT_WIDGET_AJAX ) {
		const char *header = hr->getString("header",NULL);
		if ( header ) sb->safeStrcpy ( header );
	}

	// if we are xml/json we call this below otherwise we lose
	// the header of <?xml...> or whatever
	if ( si->m_format == FORMAT_HTML ) {
		printLeftNavColumn ( *sb,st );

		printLogoAndSearchBox ( sb, &st->m_hr, si );
	}

	// the calling function checked this so it should be non-null
	const char *coll = cr->m_coll;
	int32_t collLen = strlen(coll);

	if ( si->m_format == FORMAT_WIDGET_IFRAME ||
	     si->m_format == FORMAT_WIDGET_AJAX ) {
		int32_t widgetwidth = hr->getLong("widgetwidth",150);
		int32_t widgetHeight = hr->getLong("widgetheight",400);
		//int32_t iconWidth = 25;

		// put image in this div which will have top:0px JUST like
		// the div holding the search results we print out below
		// so that the image does not scroll when you use the
		// scrollbar. holds the magifying glass img and searchbox.
		sb->safePrintf("<div class=magglassdiv "
			       "style=\"position:absolute;"
			       "right:15px;"
			       "z-index:10;"
			       "top:0px;\">");

		//int32_t refresh = hr->getLong("refresh",15);
		const char *oq = hr->getString("q",NULL);
		if ( ! oq ) oq = "";
		const char *prepend = hr->getString("prepend");
		if ( ! prepend ) prepend = "";
		const char *displayStr = "none";
		if ( prepend[0] ) displayStr = "";
		// to do a search we need to re-call the ajax,
		// just call reload like the one that is called every 15s or so
		sb->safePrintf("<form "//method=get action=/search "
			       // use "1" as arg to force reload
			       "onsubmit=\"widget123_reload(1);"

			       // let user know we are loading
			       "var w=document.getElementById("
			       "'widget123_scrolldiv');"
			       // just set the widget content to the reply
			       "if (w) "
			       "w.innerHTML='<br><br><b>Loading Results..."
			       "</b>';"

			       // prevent it from actually submitting
			       "return false;\">");

		sb->safePrintf("<img "
			       "style=\""
			       //"position:absolute;" // absolute or relative?
			       // put it on TOP of the other stuff
			       "z-index:10;"
			       "margin-top:3px;"
			       //"right:10px;"
			       //"right:2px;"
			       //"width:%" PRId32"px;"
			       // so we are to the right of the searchbox
			       "float:right;"
			       "\" "
			       "onclick=\""
			       "var e=document.getElementById('sbox');"
			       "if(e.style.display == 'none') {"
			       "e.style.display = '';"
			       // give it focus
			       "var qb=document.getElementById('qbox');"
			       "qb.focus();"
			       "} else {"
			       "e.style.display = 'none';"
			       "}"
			       "\" " // end function
			       " "
			       "width=35 "
			       "height=31 "
			       "src=\"/magglass.png\">"
			       );

		//char *origq = hr->getString("q");
		// we sort all results by spider date now so PREPEND
		// the actual user query 
		const char *origq = hr->getString("prepend");
		if ( ! origq ) origq = "";
		sb->safePrintf("<div id=sbox style=\"float:left;"
			       "display:%s;"
			       "opacity:0.83;"
			       //"background-color:gray;"
			       //"padding:5px;"
			       "\">"
			       // the box that holds the query
			       "<input type=text id=qbox name=qbox "
			       "size=%" PRId32" " //name=prepend "
			       "value=\"%s\"  "
			       "style=\"z-index:10;"
			       "font-weight:bold;"
			       "font-size:18px;"
			       "border:4px solid black;"
			       "margin:3px;"
			       "\">"
			       , displayStr
			       , widgetwidth / 23 
			       , origq
			       );
		sb->safePrintf("</div>"
			       "</form>\n"
			       );

		// . BEGIN SERP DIV
		// . div to hold the search results
		// . this will have the scrollbar to just scroll the serps
		//   and not the magnifying glass
		sb->safePrintf("</div>"
			       "<div id=widget123_scrolldiv "
			       "onscroll=widget123_append(); "
			       "style=\"position:absolute;"
			       "top:0px;"
			       "overflow-y:auto;"
			       "overflow-x:hidden;"
			       "width:%" PRId32"px;"
			       "height:%" PRId32"px;\">"
			       , widgetwidth
			       , widgetHeight);
	}

	// xml
	if ( si->m_format == FORMAT_XML )
		sb->safePrintf("<?xml version=\"1.0\" "
			      "encoding=\"UTF-8\" ?>\n"
			      "<response>\n" );

	int64_t nowMS = gettimeofdayInMilliseconds();

	// show current time
	if ( si->m_format == FORMAT_XML ) {
		sb->safePrintf("\t<currentTimeUTC>%" PRIu32"</currentTimeUTC>\n",
			      (uint32_t)(nowMS/1000));
	} 
	else if ( st->m_header && si->m_format == FORMAT_JSON ) {
	    sb->safePrintf("\"currentTimeUTC\":%" PRIu32",\n",
			   (uint32_t)(nowMS/1000));
	}

	// show result validity time
	if ( si->m_format == FORMAT_XML ) {
		int32_t expireTimeUTC = nowMS/1000 + g_conf.m_defaultQueryResultsValidityTime;
		sb->safePrintf("\t<expireTimeUTC>%" PRIu32"</expireTimeUTC>\n",
			      expireTimeUTC);
	} else if ( st->m_header && si->m_format == FORMAT_JSON ) {
		int32_t expireTimeUTC = nowMS/1000 + g_conf.m_defaultQueryResultsValidityTime;
		sb->safePrintf("\"expireTimeUTC\":%" PRIu32",\n",
			   expireTimeUTC);
	}

	// show response time if not doing Quality Assurance
	if ( si->m_format == FORMAT_XML )
		sb->safePrintf("\t<responseTimeMS>%" PRId64"</responseTimeMS>\n",
			      st->m_took);
	else if ( st->m_header && si->m_format == FORMAT_JSON )
	    sb->safePrintf("\"responseTimeMS\":%" PRId64",\n", st->m_took);

	// out of memory allocating msg20s?
	if ( st->m_errno ) {
		log("query: Query failed. Had error processing query: %s",
		    mstrerror(st->m_errno));
		g_errno = st->m_errno;
		return false;
	}


	if ( si->m_format == FORMAT_XML ) {
		sb->safePrintf("\t<numResultsOmitted>%" PRId32
			       "</numResultsOmitted>\n",
			       msg40->m_omitCount);
		sb->safePrintf("\t<numShardsSkipped>%" PRId32"</numShardsSkipped>\n",
			       msg40->m_msg3a.m_skippedShards);
		sb->safePrintf("\t<totalShards>%" PRId32"</totalShards>\n",
			       g_hostdb.m_numShards );
		sb->safePrintf("\t<pctSearched>%f</pctSearched>\n",
			       msg40->m_msg3a.m_pctSearched);
	}

	if ( st->m_header && si->m_format == FORMAT_JSON ) {
		sb->safePrintf("\"numResultsOmitted\":%" PRId32",\n",
			       msg40->m_omitCount);
		sb->safePrintf("\"numShardsSkipped\":%" PRId32",\n",
			       msg40->m_msg3a.m_skippedShards);
		sb->safePrintf("\"totalShards\":%" PRId32",\n",
			       g_hostdb.m_numShards );
		sb->safePrintf("\"pctSearched\":%f,\n",
			       msg40->m_msg3a.m_pctSearched);
	}

	// save how many docs are in this collection
	int64_t docsInColl = -1;
	RdbBase *base = getRdbBase ( RDB_CLUSTERDB , st->m_collnum );

	// estimate it
	if ( base ) {
		docsInColl = base->estimateNumGlobalRecs();
	}

	// include number of docs in the collection corpus
	if ( docsInColl >= 0LL ) {
		if ( si->m_format == FORMAT_XML) {
			sb->safePrintf ( "\t<docsInCollection>%" PRId64"</docsInCollection>\n", docsInColl );
		} else if ( st->m_header && si->m_format == FORMAT_JSON) {
			sb->safePrintf("\"docsInCollection\":%" PRId64",\n", docsInColl);
		}
	}

 	int32_t numResults = msg40->getNumResults();
	bool moreFollow = msg40->moreResultsFollow();
	// an estimate of the # of total hits
	int64_t totalHits = msg40->getNumTotalHits();
	// only adjust upwards for first page now so it doesn't keep chaning
	if ( totalHits < numResults ) totalHits = numResults;

	if ( si->m_format == FORMAT_XML )
		sb->safePrintf("\t<hits>%" PRId64"</hits>\n",(int64_t)totalHits);
	else if ( st->m_header && si->m_format == FORMAT_JSON )
		sb->safePrintf("\"hits\":%" PRId64",\n", (int64_t)totalHits);

	if ( si->m_format == FORMAT_XML )
		sb->safePrintf("\t<moreResultsFollow>%" PRId32
				"</moreResultsFollow>\n"
				,(int32_t)moreFollow);
	else if ( st->m_header && si->m_format == FORMAT_JSON )
		sb->safePrintf("\"moreResultsFollow\":%" PRId32",\n",
				(int32_t)moreFollow);

	// print individual query term info
	if ( si->m_format == FORMAT_XML ) {
		const Query *q = &si->m_q;
		sb->safePrintf("\t<queryInfo>\n");
		sb->safePrintf("\t\t<fullQuery><![CDATA[");
		cdataEncode(sb, q->originalQuery());
		sb->safePrintf("]]></fullQuery>\n");
		sb->safePrintf("\t\t<queryLanguageAbbr>"
			       "<![CDATA[%s]]>"
			       "</queryLanguageAbbr>\n"
			       , getLanguageAbbr(si->m_queryLangId) );
		sb->safePrintf("\t\t<queryLanguage>"
			       "<![CDATA[%s]]>"
			       "</queryLanguage>\n"
			       , getLanguageString(si->m_queryLangId) );
		// print query words we ignored, like stop words
		printIgnoredWords ( sb , si );

		sb->safePrintf("\t\t<queryNumTermsTotal>"
			       "%" PRId32
			       "</queryNumTermsTotal>\n"
			       , q->m_numTermsUntruncated );
		sb->safePrintf("\t\t<queryNumTermsUsed>"
			       "%" PRId32
			       "</queryNumTermsUsed>\n"
			       , q->m_numTerms );
		int32_t tval = 0;
		if ( q->m_numTerms < q->m_numTermsUntruncated ) tval = 1;
		sb->safePrintf("\t\t<queryWasTruncated>"
			       "%" PRId32
			       "</queryWasTruncated>\n"
			       , tval );

		for ( int i = 0 ; i < q->m_numTerms ; i++ ) {
			sb->safePrintf("\t\t<term>\n");
			const QueryTerm *qt = &q->m_qterms[i];
			sb->safePrintf("\t\t\t<termNum>%i</termNum>\n",i);
			const char *term = qt->m_term;
			sb->safePrintf("\t\t\t<termStr><![CDATA[");
			const char *printTerm = qt->m_term;
			if ( is_wspace_a(term[0])) printTerm++;
			cdataEncode(sb, printTerm,qt->m_termLen);
			sb->safePrintf("]]>"
				       "</termStr>\n");
			// syn?
			const QueryTerm *sq = qt->m_synonymOf;
			// what language did synonym come from?
			if ( sq ) {
				// language map from wiktionary
				sb->safePrintf("\t\t\t<termLang>"
					       "<![CDATA[");
				bool first = true;
				for ( int i = 0 ; i < langLast ; i++ ) {
					uint64_t bit = (uint64_t)1 << i;
					if ( ! (qt->m_langIdBits&bit))continue;
					const char *str = getLanguageAbbr(i);
					if ( ! first ) sb->pushChar(',');
					first = false;
					sb->safeStrcpy ( str );
				}
				sb->safePrintf("]]></termLang>\n");
			}

			if ( sq ) {
				const char *term = sq->m_term;
				const char *printTerm = term;
				if ( is_wspace_a(term[0])) printTerm++;
				sb->safePrintf("\t\t\t<synonymOf>"
					       "<![CDATA[%*.*s]]>"
					       "</synonymOf>\n"
					       ,(int)sq->m_termLen,(int)sq->m_termLen,printTerm);
			}				
			sb->safePrintf("\t\t\t<termFreq>%" PRId64"</termFreq>\n"
				       ,qt->m_termFreq);
			sb->safePrintf("\t\t\t<termFreqWeight>%.2f</termFreqWeight>\n"
				       ,qt->m_termFreqWeight);
			sb->safePrintf("\t\t\t<termHash48>%" PRId64"</termHash48>\n"
				       ,qt->m_termId);
			sb->safePrintf("\t\t\t<termHash64>%" PRIu64"</termHash64>\n"
				       ,qt->m_rawTermId);
			const QueryWord *qw = qt->m_qword;
			sb->safePrintf("\t\t\t<prefixHash64>%" PRIu64"</prefixHash64>\n"
				       ,qw->m_prefixHash);
			sb->safePrintf("\t\t</term>\n");
		}
		sb->safePrintf("\t</queryInfo>\n");
	}			

	// print individual query term info
	if ( si->m_format == FORMAT_JSON && st->m_header ) {
		const Query *q = &si->m_q;
		sb->safePrintf("\"queryInfo\":{\n");
		sb->safePrintf("\t\"fullQuery\":\"");
		sb->jsonEncode(q->originalQuery());
		sb->safePrintf("\",\n");
		sb->safePrintf("\t\"queryLanguageAbbr\":\"");
		sb->jsonEncode ( getLanguageAbbr(si->m_queryLangId) );
		sb->safePrintf("\",\n");
		sb->safePrintf("\t\"queryLanguage\":\"");
		sb->jsonEncode ( getLanguageString(si->m_queryLangId) );
		sb->safePrintf("\",\n");
		
		//If the language weights are dissimilar then print the top 5
		//make a temporari sorted-by-weight list of languages
		struct langweight_t {
			lang_t lang;
			double weight;
		} langweights[MAX_LANGUAGES];
		for(int i=0; i<MAX_LANGUAGES; i++) {
			langweights[i].lang = (lang_t)i;
			langweights[i].weight = si->m_baseScoringParameters.m_languageWeights[i];
		}
		std::sort(langweights, langweights+MAX_LANGUAGES, [](const langweight_t& a, const langweight_t& b) {
			return a.weight>b.weight;
		});
		//if the weigths are not all identical then print the top 5
		if(langweights[0].weight!=langweights[MAX_LANGUAGES-1].weight) {
			sb->safePrintf("\t\"languageWeights\": [ ");
			for(int i=0; i<7; i++) {
				const char *l = getLanguageAbbr(langweights[i].lang);
				sb->safePrintf("{\"lang\":\"%s\", \"weight\":%.3f}", l, langweights[i].weight);
				if(i+1<7)
					sb->safePrintf(", ");
			}
			sb->safePrintf(" ],\n");
		}
		
		// print query words we ignored, like stop words
		printIgnoredWords ( sb , si );

		sb->safePrintf("\t\"queryNumTermsTotal\":"
			       "%" PRId32",\n"
			       , q->m_numTermsUntruncated );
		sb->safePrintf("\t\"queryNumTermsUsed\":"
			       "%" PRId32",\n"
			       , q->m_numTerms );
		int32_t tval = 0;
		if ( q->m_numTerms < q->m_numTermsUntruncated ) tval = 1;
		sb->safePrintf("\t\"queryWasTruncated\":"
			       "%" PRId32",\n"
			       , tval );
			
		sb->safePrintf("\t\"terms\":[\n");
		for ( int i = 0 ; i < q->m_numTerms ; i++ ) {
			const QueryTerm &qt = q->m_qterms[i];
			sb->safePrintf("\t\t{\n");
			sb->safePrintf("\t\t\t\"termNum\":%i,\n",i);
			sb->safePrintf("\t\t\t\"termStr\":\"");
			sb->jsonEncode(qt.m_term, qt.m_termLen);
			sb->safePrintf("\",\n");
			sb->safePrintf("\t\t\t\"isPhrase\":%s,\n", qt.m_isPhrase?"true":"false");
			sb->safePrintf("\t\t\t\"termHash48\":%" PRId64",\n",   qt.m_termId);
			sb->safePrintf("\t\t\t\"termHash64\":%" PRIu64",\n",    qt.m_rawTermId);
			//m_termSign?
			sb->safePrintf("\t\t\t\"termFreq\":%" PRId64",\n",      qt.m_termFreq);
			sb->safePrintf("\t\t\t\"termFreqWeight\":%.2f,\n",      qt.m_termFreqWeight);
			sb->safePrintf("\t\t\t\"queryStopWord\":%s,\n",         qt.m_isQueryStopWord?"true":"false");
			sb->safePrintf("\t\t\t\"termWeight\":%.2f,\n",          qt.m_termWeight);
			sb->safePrintf("\t\t\t\"userWeight\":%.2f,\n",          qt.m_userWeight);
			sb->safePrintf("\t\t\t\"userNotRequired\":%s,\n",       qt.m_userNotRequired?"true":"false");
			sb->safePrintf("\t\t\t\"ignored\":%s,\n",               qt.m_ignored?"true":"false");
			const QueryTerm *synqt = qt.m_synonymOf;
			if(synqt) {
				if(qt.m_langIdBitsValid) {
					// what language did synonym come from?
					// language map from wiktionary
					sb->safePrintf("\t\t\t\"termLang\":\"");
					bool first = true;
					for(int i = 0; i < langLast; i++) {
						uint64_t bit = (uint64_t)1 << i;
						if(!(qt.m_langIdBits&bit))
							continue;
						const char *str = getLanguageAbbr(i);
						if(!first)
							sb->pushChar(',');
						first = false;
						sb->jsonEncode(str);
					}
					sb->safePrintf("\",\n");
				}
				
				sb->safePrintf("\t\t\t\"synonymOf\":\"");
				sb->jsonEncode(synqt->m_term, synqt->m_termLen);
				sb->safePrintf("\",\n");
			}
			const char *fieldCodeName = getFieldCodeName(qt.m_fieldCode);
			if(fieldCodeName)
				sb->safePrintf("\t\t\t\"fieldCodeName\":\"%s\",\n", fieldCodeName);
			sb->safePrintf("\t\t\t\"required\":%s,\n",              qt.m_isRequired?"true":"false");

			sb->safePrintf("\t\t\t\"prefixHash64\":%" PRIu64"\n", qt.m_qword->m_prefixHash);

			sb->safePrintf("\t\t}");
			// don't end last query term attr on a omma
			if( i + 1 < q->m_numTerms)
				sb->pushChar(',');
			sb->pushChar('\n');
		}
		sb->safePrintf("\t]\n"); // end "terms":[]
		sb->safePrintf("},\n"); //end "queryInfo":{}
	}

	if ( si->m_format == FORMAT_JSON && st->m_header ) {
		sb->safePrintf("\"results\":[\n");
		return true;
	}

	// debug
	if ( si->m_debug ) {
		logf(LOG_DEBUG,"query: Displaying up to %" PRId32" results.", numResults);
	}

	// get some result info from msg40
	int32_t firstNum   = msg40->getFirstResultNum() ;

	// numResults may be more than we requested now!
	int32_t n = msg40->getDocsWanted();
	if ( n > numResults )  n = numResults;

	// . make the query class here for highlighting
	// . keepAllSingles means to convert all individual words into
	//   QueryTerms even if they're in quotes or in a connection (cd-rom).
	//   we use this for highlighting purposes
	Query qq;
	qq.set(si->m_displayQuery, langUnknown, 1.0, 1.0, &si->m_word_variations_config, false,
	       si->m_allowHighFrequencyTermCache, ABS_MAX_QUERY_TERMS);
	//syn-todo: in the call above si->m_queryExpansion was used for both 'queryExpansion' and 'useQueryStopWords'. Why?

	if ( g_errno ) return false;

	DocIdScore *dpx = NULL;
	if ( numResults > 0 ) dpx = msg40->getScoreInfo(0);

	if ( si->m_format == FORMAT_XML && dpx ) {
		float max = 0.0;

		// max pairwise
		float lw = getHashGroupWeight(HASHGROUP_INLINKTEXT);

		// square that location weight
		lw *= lw;

		// assume its an inlinker's text, who has rank 15!!!
		lw *= getLinkerWeight(MAXSITERANK);

		// single weights
		float maxtfw1 = 0.0;
		int32_t maxi1;

		// now we can have multiple SingleScores for the same term!
		// because we take the top MAX_TOP now and add them to
		// get the term's final score.
		for ( int32_t i = 0 ; i< dpx->m_numSingles ; i++ ) {
			SingleScore *ssi = &dpx->m_singleScores[i];
			float tfwi = ssi->m_tfWeight;
			if ( tfwi <= maxtfw1 ) continue;
			maxtfw1 = tfwi;
			maxi1 = i;
		}
		float maxtfw2 = 0.0;
		for ( int32_t i = 0 ; i< dpx->m_numSingles ; i++ ) {
			if ( i == maxi1 ) continue;
			SingleScore *ssi = &dpx->m_singleScores[i];
			float tfwi = ssi->m_tfWeight;
			if ( tfwi <= maxtfw2 ) continue;
			maxtfw2 = tfwi;
		}
		// only 1 term?
		if ( almostEqualFloat(maxtfw2, 0.0) ) maxtfw2 = maxtfw1;
		// best term freqs
		max *= maxtfw1 * maxtfw2;
		// site rank effect
		max *= MAXSITERANK*si->m_baseScoringParameters.m_siteRankMultiplier + 1;
		sb->safePrintf ("\t\t<theoreticalMaxFinalScore>%f"
			       "</theoreticalMaxFinalScore>\n",
			       max );
	}
	


	// debug msg
	log ( LOG_TIMING ,
	     "query: Got %" PRId32" search results in %" PRId64" ms for q=%s",
	      numResults,gettimeofdayInMilliseconds()-st->m_startTime,
	      qq.getQuery());

	st->m_qesb.nullTerm();

	// encode query buf
	const char *dq    = si->m_displayQuery;
	if ( dq ) {
		urlEncode(&st->m_qesb,dq);
	}

	// print it with commas into "thbuf" and null terminate it
	char thbuf[64];
	ulltoa ( thbuf , totalHits );

	char inbuf[128];
	ulltoa ( inbuf , docsInColl );

	// otherwise, we had no error
	if ( numResults == 0 && si->m_format == FORMAT_HTML ) {
		sb->safePrintf ( "No results found in <b>%s</b> collection.",
				cr->m_coll);
	}
	// the token is currently in the collection name so do not show that
	else if ( numResults == 0 && 
		  ( si->m_format == FORMAT_WIDGET_IFRAME ||
		    si->m_format == FORMAT_WIDGET_AJAX ) ) {
		sb->safePrintf ( "No results found. Wait for spider to "
				 "kick in.");
	}
	else if ( moreFollow && si->m_format == FORMAT_HTML ) {
		sb->safePrintf ("Results <b>%" PRId32"</b> to <b>%" PRId32"</b> of "
			       "exactly <b>%s</b> from an index "
			       "of about %s pages" , 
			       firstNum + 1          ,
			       firstNum + n          ,
			       thbuf                 ,
			       inbuf
			       );
	}
	// otherwise, we didn't get enough results to show this page
	else if ( si->m_format == FORMAT_HTML ) {
		sb->safePrintf ("Results <b>%" PRId32"</b> to <b>%" PRId32"</b> of "
			       "exactly <b>%s</b> from an index "
			       "of about %s pages" , 
			       firstNum + 1          ,
			       firstNum + n          ,
			       thbuf                 ,
			       inbuf
			       );
	}

	if ( si->m_format == FORMAT_HTML )
		sb->safePrintf(" in %.02f seconds",((float)st->m_took)/1000.0);


	//
	// if query was a url print add url msg
	//
	const char *url = NULL;
	if ( !strncmp(q,"url:"    ,4) && qlen > 4 ) url = q+4;
	if ( !strncmp(q,"http://" ,7) && qlen > 7 ) url = q;
	if ( !strncmp(q,"https://",8) && qlen > 8 ) url = q;
	if ( !strncmp(q,"www."    ,4) && qlen > 4 ) url = q;
	// find end of url
	const char *ue = url;
	for ( ; ue && *ue && ! is_wspace_a(*ue) ; ue++ ) ;
	if ( numResults == 0 && si->m_format == FORMAT_HTML && url ) {
		sb->safePrintf("<br><br>"
			      "Could not find that url in the "
			      "index. Try <a href=\"/addurl?u=");
		urlEncode(sb,url,ue-url,false,false);
		sb->safePrintf("\">Adding it.</a>");
	}

	// sometimes ppl search for "www.whatever.com" so ask them if they
	// want to search for url:www.whatever.com
	if ( numResults > 0  && si->m_format == FORMAT_HTML && url && url == q){
		sb->safePrintf("<br><br>"
			      "Did you mean to "
			      "search for the url "
			      "<a href=\"/search?q=url%%3A");
		urlEncode(sb,url,ue-url,false,false);
		sb->safePrintf("\">");
		sb->safeMemcpy(url,ue-url);
		sb->safePrintf("</a> itself?");
	}


	// is it the main collection?
	bool isMain = false;
	if ( collLen == 4 && strncmp ( coll, "main", 4) == 0 ) isMain = true;

	// print "in collection ***" if we had a collection
	if (collLen>0 && numResults>0 && !isMain && si->m_format==FORMAT_HTML )
		sb->safePrintf (" in collection <b>%s</b>",coll);


	printIgnoredWords ( sb , si );


	if ( si->m_format == FORMAT_HTML ) {
		sb->safePrintf("<br><br>");

		sb->safePrintf("<table cellpadding=0 cellspacing=0>"
			      "<tr><td valign=top>");
	}

	// debug
	if ( si->m_debug )
		logf(LOG_DEBUG,"query: Printing up to %" PRId32" results. "
		     "bufStart=%p",
		     numResults,
		     sb->getBufPtr());

	return true;
}


bool printSearchResultsTail(State0 *st) {
	SafeBuf *sb = &st->m_sb;

	SearchInput *si = &st->m_si;	

	Msg40 *msg40 = &(st->m_msg40);

	CollectionRec *cr = si->m_cr;
	const char *coll = cr->m_coll;

	if ( si->m_format == FORMAT_JSON ) {	
		// remove last },\n if there and replace with just \n
		const char *e = sb->getBufPtr() - 2;
		if ( sb->length()>=2 &&
		     e[0]==',' && e[1]=='\n') {
			sb->m_length -= 2;
			sb->safePrintf("\n");
		}
		// print ending ] for json search results
		sb->safePrintf("]\n");

		if ( st->m_header ) sb->safePrintf("}\n");

		//////////////////////
		// for some reason if we take too long to write out this
		// tail we get a SIGPIPE on a firefox browser.
		//////////////////////

		// all done for json
		return true;
	}

	// grab the query
	const char  *q    = msg40->getQuery();
	int32_t   qlen = msg40->getQueryLen();

	HttpRequest *hr = &st->m_hr;

	// get some result info from msg40
	int32_t firstNum   = msg40->getFirstResultNum() ;

	// end the two-pane table
	if ( si->m_format == FORMAT_HTML) sb->safePrintf("</td></tr></table>");

	//
	// PRINT PREV 10 NEXT 10 links!
	// 

	// center everything below here
	if ( si->m_format == FORMAT_HTML ) sb->safePrintf ( "<br><center>" );

	int32_t remember = sb->length();

	// now print "Prev X Results" if we need to
	if ( firstNum < 0 ) firstNum = 0;

	StackBuf<300> args;
	// show banned?
	if ( si->m_showBanned && ! si->m_isMasterAdmin )
		args.safePrintf("&sb=1");
	if ( ! si->m_showBanned && si->m_isMasterAdmin )
		args.safePrintf("&sb=0");

	// collection
	args.safePrintf("&c=%s",coll);
	// formatting info
	if ( si->m_format == FORMAT_WIDGET_IFRAME ||
	     si->m_format == FORMAT_WIDGET_AJAX ) {
		args.safePrintf("&format=widget");
		int32_t widgetwidth = hr->getLong("widgetwidth",250);
		args.safePrintf("&widgetwidth=%" PRId32,widgetwidth);
	}

	// carry over the sites we are restricting the search results to
	if ( si->m_sites )
		//whiteListBuf.getBufStart());
		args.safePrintf("&sites=%s",si->m_sites);


	if ( si->m_format == FORMAT_HTML &&
	     msg40->m_omitCount ) { // && firstNum == 0 ) { 
		// . add our cgi to the original url
		// . so if it has &qlang=de and they select &qlang=en
		//   we have to replace it... etc.
		StackBuf<> newUrl;
		// show banned results
		replaceParm2 ("sb=1",
			      &newUrl,
			      hr->getOrigUrlRequest(),
			      hr->getOrigUrlRequestLen() );
		// no deduping by summary or content hash etc.
		StackBuf<> newUrl2;
		replaceParm2("dr=0",&newUrl2,newUrl.getBufStart(),
			     newUrl.length());
		// and no site clustering
		StackBuf<> newUrl3;
		replaceParm2 ( "sc=0", &newUrl3 , newUrl2.getBufStart(),
			     newUrl2.length());
		// start at results #0 again
		StackBuf<> newUrl4;
		replaceParm2 ( "s=0", &newUrl4 , newUrl3.getBufStart(),
			     newUrl3.length());
		// show errors
		StackBuf<> newUrl5;
		replaceParm2 ( "showerrors=1", 
			       &newUrl5 , 
			       newUrl4.getBufStart(),
			       newUrl4.length());
		
		
		sb->safePrintf("<center>"
			       "<i>"
			       "%" PRId32" results were omitted because they "
			       "were considered duplicates, banned, errors "
			       "<br>"
			       "or "
			       "from the same site as other results. "
			       "<a href=%s>Click here to show all results</a>."
			       "</i>"
			       "</center>"
			       "<br><br>"
			       , msg40->m_omitCount
			       , newUrl5.getBufStart() );
	}


	if ( firstNum > 0 && 
	     (si->m_format == FORMAT_HTML || 
	      si->m_format == FORMAT_WIDGET_IFRAME //||
	      //si->m_format == FORMAT_WIDGET_AJAX
	      ) ) {
		int32_t ss = firstNum - msg40->getDocsWanted();
		
		//sb->safePrintf("<a href=\"/search?s=%" PRId32"&q=",ss);
		// our current query parameters
		//sb->safeStrcpy ( st->m_qe );
		// print other args if not zero
		//sb->safeMemcpy ( &args );

		// make the cgi parm to add to the original url
		char nsbuf[128];
		sprintf(nsbuf,"s=%" PRId32,ss);
		// get the original url and add/replace in &s=xxx
		StackBuf<> newUrl;
		replaceParm ( nsbuf , &newUrl , hr );


		// close it up
		sb->safePrintf ("<a href=\"%s\"><b>"
			       "<font size=+0>Prev %" PRId32" Results</font>"
			       "</b></a>"
				, newUrl.getBufStart()
				, msg40->getDocsWanted() );
	}

	// now print "Next X Results"
	if ( msg40->moreResultsFollow() && 
	     (si->m_format == FORMAT_HTML || 
	      si->m_format == FORMAT_WIDGET_IFRAME 
	      //si->m_format == FORMAT_WIDGET_AJAX 
	      )) {
		int32_t ss = firstNum + msg40->getDocsWanted();
		// print a separator first if we had a prev results before us
		if ( sb->length() > remember ) sb->safePrintf ( " &nbsp; " );
		// add the query
		//sb->safePrintf ("<a href=\"/search?s=%" PRId32"&q=",ss);
		// our current query parameters
		//sb->safeStrcpy ( st->m_qe );
		// print other args if not zero
		//sb->safeMemcpy ( &args );

		// make the cgi parm to add to the original url
		char nsbuf[128];
		sprintf(nsbuf,"s=%" PRId32,ss);
		// get the original url and add/replace in &s=xxx
		StackBuf<> newUrl;
		replaceParm ( nsbuf , &newUrl , hr );

		// close it up
		sb->safePrintf("<a href=\"%s\"><b>"
			      "<font size=+0>Next %" PRId32" Results</font>"
			      "</b></a>"
			       , newUrl.getBufStart()
			       , msg40->getDocsWanted() );
	}


	// print try this search on...
	// an additional <br> if we had a Next or Prev results link
	if ( sb->length() > remember &&
	     si->m_format == FORMAT_HTML ) 
		sb->safeMemcpy ("<br>" , 4 ); 

	//
	// END PRINT PREV 10 NEXT 10 links!
	// 

	if ( si->m_format == FORMAT_HTML ) {
		sb->safePrintf("<input name=c type=hidden value=\"%s\">",coll);
	}

	bool isAdmin = (si->m_isMasterAdmin || si->m_isCollAdmin);
	if ( si->m_format != FORMAT_HTML ) isAdmin = false;

	// TODO: print cache line in light gray here
	// TODO: "these results were cached X minutes ago"
	if ( msg40->getCachedTime() > 0 && si->m_format == FORMAT_HTML ) {
		sb->safePrintf("<br><br><font size=1 color=707070>"
			       "<b><center>");
		sb->safePrintf ( " These results were cached " );
		// this cached time is this local cpu's time
		int32_t diff = getTime() - msg40->getCachedTime();
		if      ( diff < 60   ) sb->safePrintf ("%" PRId32" seconds", diff );
		else if ( diff < 2*60 ) sb->safePrintf ("1 minute");
		else                    sb->safePrintf ("%" PRId32" minutes",diff/60);
		sb->safePrintf ( " ago. [<a href=\"/pageCache.html\">"
				"<font color=707070>Info</font></a>]");
		sb->safePrintf ( "</center></font>");
	}

	

	if ( si->m_format == FORMAT_XML ) {
		sb->safePrintf("</response>\n");
	}

	if ( si->m_format == FORMAT_HTML && 
	     cr->m_htmlTail.length() == 0 ) {
		sb->safePrintf ( "<br>"
				 "<center>"
				 "<font color=gray>"
				 "Copyright &copy; 2014. All Rights "
				 "Reserved.<br/>"
				 "Powered by the <a href=\"http://www."
				 "gigablast.com/\">GigaBlast</a> open source "
				 "search engine."
				 "</font>"
				 "</center>\n"
				 "<br>\n"
				 );
	}


	// if we did not use ajax, print this tail here now
	if ( si->m_format == FORMAT_HTML ) {
		sb->safePrintf( "</body>\n"
				"</html>\n"
				);
	}

	// ajax widgets will have this outside the downloaded content
	if ( si->m_format == FORMAT_WIDGET_IFRAME ) {
		sb->safePrintf ( "<br>"
				"<center>"
				"<font color=gray>"
				 // link to edit the list of widget sites
				 // or various other widget content properties
				 // because we can't edit the width/height
				 // of the widget like this.
				 "<a href=\"/widget?inlineedit=1\">edit</a> "
				 "&bull; "
				 //"Copyright &copy; 2014. All Rights "
				 //"Reserved.<br/>"
				"Powered by <a href=http://www.diffbot.com>"
				 "Diffbot</a>."
				"</font>"
				"</center>\n"
				"</body>\n"
				"</html>\n"
				 );
	}

	if ( sb->length() == 0 && si->m_format == FORMAT_JSON )
		sb->safePrintf("[]\n");

	if ( sb->length() == 0 ) {
		sb->pushChar('\n');
		sb->nullTerm();
	}

	if ( si->m_format == FORMAT_HTML &&
	     cr->m_htmlTail.length() &&
	     ! expandHtml ( *sb ,
			    cr->m_htmlTail.getBufStart(),
			    cr->m_htmlTail.length(),
			    q,
			    qlen,
			    hr,
			    si,
			    NULL, // method,
			    cr) )
			return false;

	return true;
}


static int linkSiteRankCmp (const void *v1, const void *v2) {
	Inlink *i1 = *(Inlink **)v1;
	Inlink *i2 = *(Inlink **)v2;
	return i2->m_siteRank - i1->m_siteRank;
}

static bool printInlinkText ( SafeBuf *sb , Msg20Reply *mr , SearchInput *si ,
		       int32_t *numPrinted ) {
	*numPrinted = 0;
	// . show the "LinkInfo"
	// . Msg20.cpp will have "computed" the LinkInfo if we set
	//   Msg20Request::m_computeLinkInfo to true, but if we set
	//   Msg20Request::m_getLinkInfo to true it will just get it
	//   from the TitleRec, which is much faster but more stale.
	// . "&inlinks=1" is slow and fresh, "&inlinks=2" is fast
	//   and stale. Both are really only for BuzzLogic.
	LinkInfo *info = (LinkInfo *)mr->ptr_linkInfo;//inlinks;
	// Corrupted linkinfo has been seen. In that case just log an error and do not try to print any link texts
	if(info && mr->size_linkInfo!=info->m_lisize) {
		log(LOG_ERROR, "results: mr->size_linkInfo(%d) != info->m_lisize (%d)", mr->size_linkInfo, info->m_lisize);
		return false;
	}

	// NULLify if empty
	if ( mr->size_linkInfo <= 0 ) info = NULL;
	// do not both if none
	if ( info && ! info->m_numStoredInlinks ) info = NULL;
	// bail?
	if ( ! info ) return true;
	// now sort them up
	Inlink *ptrs[3000]; //could use #define from Msg25/linkdb.h but we don't really have to match
	int32_t numLinks = 0;
	for(Inlink *k = info->getNextInlink(NULL); k; k = info->getNextInlink(k)) {
		ptrs[numLinks++] = k;
		if ( numLinks >= 3000 ) break;
	}
	// sort them
	gbsort ( ptrs , numLinks , sizeof(Inlink *) , linkSiteRankCmp );
	// print xml starter
	if ( si->m_format == FORMAT_XML ) sb->safePrintf("\t\t<inlinks>\n");
	// loop through the inlinks
	bool printedInlinkText = false;
	bool firstTime = true;
	int32_t inlinkId = 0;
	int64_t  starttime = gettimeofdayInMilliseconds();

	for ( int32_t i = 0 ; i < numLinks ; i++ ) {
		Inlink *k = ptrs[i];
		if ( ! k->getLinkText() ) continue;
		if ( ! si->m_doQueryHighlighting && 
		     si->m_format == FORMAT_HTML ) 
			continue;
		const char *str   = k->getLinkText();//ptr_linkText;
		int32_t strLen = strnlen(k->getLinkText(),k->size_linkText);

		const char *frontTag =
		     "<font style=\"color:black;background-color:yellow\">" ;
		const char *backTag = "</font>";
		if ( si->m_format == FORMAT_XML ) {
			frontTag = "<b>";
			backTag  = "</b>";
		}
		if ( si->m_format == FORMAT_WIDGET_IFRAME ||
		     si->m_format == FORMAT_WIDGET_AJAX ) {
			frontTag = "<font style=\"background-color:yellow\">" ;
		}

		Highlight hi;
		SafeBuf hb;
		int32_t hlen = hi.set ( &hb, str, strLen , &si->m_hqq, frontTag, backTag );
		if ( hlen <= 0 ) {
			continue;
		}

		// skip it if nothing highlighted
		if ( hi.getNumMatches() == 0 ) continue;

		if ( si->m_format == FORMAT_XML ) {
			sb->safePrintf("\t\t\t<inlink "
				      "docId=\"%" PRId64"\" "
				      "url=\"",
				      k->m_docId );
			// encode it for xml
			sb->htmlEncode ( k->getUrl(),//ptr_urlBuf,
					k->size_urlBuf - 1 , false );
			sb->safePrintf("\" "
				      //"hostId=\"%" PRIu32"\" "
				      "firstindexed=\"%" PRIu32"\" "
				      // not accurate!
				      //"lastspidered=\"%" PRIu32"\" "
				      "wordposstart=\"%" PRId32"\" "
				      "id=\"%" PRId32"\" "
				      "siterank=\"%" PRId32"\" "
				      "text=\"",
				      //hh ,
				      //(int32_t)k->m_datedbDate,
				      (uint32_t)k->m_firstIndexedDate,
				      //(uint32_t)k->m_lastSpidered,
				      (int32_t)k->m_wordPosStart,
				      inlinkId,
				      //linkScore);
				      (int32_t)k->m_siteRank
				      );
			// HACK!!!
			k->m_siteHash = inlinkId;
			// inc it
			inlinkId++;
			// encode it for xml
			if ( !sb->htmlEncode ( hb.getBufStart(),
					      hb.length(),
					      false)) 
				return false;
			sb->safePrintf("\"/>\n");
			continue;
		}


		if ( firstTime ) {
			sb->safePrintf("<font size=-1>");
			sb->safePrintf("<table border=1>"
				      "<tr><td colspan=10>"
				      "<center>"
				      "<b>Inlinks with Query Terms</b>"
				      "</center>"
				      "</td></tr>"
				      "<tr>"
				      "<td>Inlink Text</td>"
				      "<td>From Site</td>"
				      "<td>Site IP</td>"
				      "<td>Site Rank</td>"
				      "</tr>"
				      );
		}
		firstTime = false;
		sb->safePrintf("<tr><td>"
			      "<a href=\"/get?c=%s&d=%" PRId64"&cnsp=0\">"
			      //"<a href=\"/print?"
			      //"page=7&"
			      //"c=%s&"
			      //"d=%" PRId64"\">"
			      //k->getUrl());
			      ,si->m_cr->m_coll
			      ,k->m_docId);
		if ( ! sb->safeMemcpy(&hb) ) return false;
		int32_t hostLen = 0;
		const char *host = getHostFast(k->getUrl(),&hostLen,NULL);
		sb->safePrintf("</td><td>");
		if ( host ) sb->safeMemcpy(host,hostLen);
		sb->safePrintf("</td><td>");
		char ipbuf[16];
		sb->safePrintf("<a href=\"/search?c=%s&q=ip%%3A%s"
			       "&n=100\">"
			       ,si->m_cr->m_coll,iptoa(k->m_ip,ipbuf));
		sb->safePrintf("%s</a>",iptoa(k->m_ip,ipbuf));
		sb->safePrintf("</td><td>%" PRId32"</td></tr>"
			       ,(int32_t)k->m_siteRank);
		//sb->safePrintf("<br>");
		printedInlinkText = true;
		*numPrinted = *numPrinted + 1;
	}

	int64_t took = gettimeofdayInMilliseconds() - starttime;
        if ( took > 2 )
                log("timing: took %" PRId64" ms to highlight %" PRId32" links."
                    ,took,numLinks);


	// closer for xml
	if ( si->m_format == FORMAT_XML ) sb->safePrintf("\t\t</inlinks>\n");
	//if ( printedInlinkText ) sb->safePrintf("<br>\n");
	if ( printedInlinkText )
		sb->safePrintf("</font>"
			      "</table>"
			      "<br>");
	return true;
}

// use this for xml as well as html
static bool printResult(State0 *st, int32_t ix, int32_t *numPrintedSoFar) {
	SafeBuf *sb = &st->m_sb;

	HttpRequest *hr = &st->m_hr;

	CollectionRec *cr = g_collectiondb.getRec ( st->m_collnum );
	if ( ! cr ) {
		log("query: printResult: collnum %" PRId32" gone",
		    (int32_t)st->m_collnum);
		return true;
	}


	// shortcuts
	SearchInput *si    = &st->m_si;
	Msg40       *msg40 = &st->m_msg40;

	// ensure not all cluster levels are invisible
	if ( si->m_debug )
		logf(LOG_DEBUG,"query: result #%" PRId32" clusterlevel=%" PRId32,
		     ix, (int32_t)msg40->getClusterLevel(ix));

	int64_t d = msg40->getDocId(ix);
	// this is normally a double, but cast to float
	float docScore = (float)msg40->getScore(ix);
	unsigned docFlags = msg40->getFlags(ix);

	if ( si->m_docIdsOnly ) {
		if ( si->m_format == FORMAT_XML ) {
			sb->safePrintf("\t<result>\n"
				       "\t\t<docId>%" PRId64"</docId>\n"
				       "\t</result>\n", 
				       d );
		}
		else
		if ( si->m_format == FORMAT_JSON ) {
			sb->safePrintf("\t{\n"
				       "\t\t\"docId\":%" PRId64"\n"
				       "\t},\n",
				       d );
		}
		else {
			sb->safePrintf("%" PRId64"<br/>\n", d );
		}

		// inc it
		*numPrintedSoFar = *numPrintedSoFar + 1;
		return true;
	}

	Msg20 *m20 = msg40->m_msg20[ix];

	// get the reply
	Msg20Reply *mr = m20->m_r;
		

	// . sometimes the msg20reply is NULL so prevent it coring
	// . i think this happens if all hosts in a shard are down or timeout
	//   or something
	if ( ! mr ) {
		if(si->m_format == FORMAT_HTML)
			sb->safePrintf("<i>getting summary for docid %" PRId64" had error: %s</i><br>\n", d, mstrerror(m20->m_errno));
		return true;
	}


	int32_t cursor = -1;
	if ( si->m_format == FORMAT_XML  ) cursor = sb->length();
	if ( si->m_format == FORMAT_JSON ) cursor = sb->length();

	if ( si->m_format == FORMAT_XML ) 
		sb->safePrintf("\t<result>\n" );

	if ( si->m_format == FORMAT_JSON ) {
		if ( *numPrintedSoFar != 0 ) {
			sb->safePrintf(",\n");
		}
		sb->safePrintf("\t{\n" );
	}


	if ( mr->ptr_content && si->m_format == FORMAT_XML ) {
		sb->safePrintf("\t\t<content><![CDATA[");
		cdataEncode(sb, mr->ptr_content);
		sb->safePrintf("]]></content>\n");
	}
		
	if ( mr->ptr_content && si->m_format == FORMAT_JSON ) {
		sb->safePrintf("\t\t\"content\":\"");
		sb->jsonEncode(mr->ptr_content);
		sb->safePrintf("\",\n");
	}

	Highlight hi;

	// get the url
	const char *url = mr->ptr_ubuf;
	int32_t urlLen = mr->size_ubuf - 1;
	int32_t err = mr->m_errno;

	// . remove any session ids from the url
	// . for speed reasons, only check if its a cgi url
	Url uu;
	uu.set( url, urlLen, false, true );
	url    = uu.getUrl();
	urlLen = uu.getUrlLen();

	bool isAdmin = (si->m_isMasterAdmin || si->m_isCollAdmin);
	if ( si->m_format == FORMAT_XML ) isAdmin = false;

	if ( si->m_format == FORMAT_HTML ) {
		sb->safePrintf("<table><tr><td>");
	}

	if ( si->m_showBanned ) {
		if ( err == EDOCBANNED   ) err = 0;
		if ( err == EDOCFILTERED ) err = 0;
	}

	// if this msg20 had an error print "had error"
	if ( err || urlLen <= 0 || ! url ) {
		// revert back so we do not break the json/xml
		if ( cursor >= 0 ) sb->m_length = cursor;
		// it's unprofessional to display this in browser
		// so just let admin see it
		if ( isAdmin && si->m_format == FORMAT_HTML ) {
			sb->safePrintf("<i>docId %" PRId64" had error: %s</i><br><br>", mr->m_docId, mstrerror(err));
		}
		log("query: docId %" PRId64" had error: %s.", mr->m_docId,mstrerror(err));
		// wrap it up if clustered
		// DO NOT inc it otherwise puts a comma in there and
		// screws up the json
		//*numPrintedSoFar = *numPrintedSoFar + 1;
		return true;
	}

	// if we have a thumbnail show it next to the search result,
	// base64 encoded. do NOT do this for the WIDGET, only for search
	// results in html/xml.
	if ( (si->m_format == FORMAT_HTML || si->m_format == FORMAT_XML ) && si->m_showImages && mr->ptr_imgData ) {
		ThumbnailArray *ta = (ThumbnailArray *)mr->ptr_imgData;
		ThumbnailInfo *ti = ta->getThumbnailInfo(0);
		if ( si->m_format == FORMAT_XML )
			sb->safePrintf("\t\t");
		ti->printThumbnailInHtml ( sb , 
					   100 ,  // max width
					   100 ,  // max height
					   true ,  // add <a href>
					   NULL ,
					   " style=\"margin:10px;\" ",
					   si->m_format );
		if ( si->m_format == FORMAT_XML ) {
			sb->safePrintf("\t\t<imageHeight>%" PRId32"</imageHeight>\n",
				       ti->m_dy);
			sb->safePrintf("\t\t<imageWidth>%" PRId32"</imageWidth>\n",
				       ti->m_dx);
			sb->safePrintf("\t\t<origImageHeight>%" PRId32
				       "</origImageHeight>\n",
				       ti->m_origDY);
			sb->safePrintf("\t\t<origImageWidth>%" PRId32
				       "</origImageWidth>\n",
				       ti->m_origDX);
			sb->safePrintf("\t\t<imageUrl><![CDATA[");
			cdataEncode(sb, ti->getUrl());
			sb->safePrintf("]]></imageUrl>\n");
		}
		if ( si->m_format == FORMAT_JSON ) {
			sb->safePrintf("\t\t\"imageHeight\":%" PRId32",\n",
				       ti->m_dy);
			sb->safePrintf("\t\t\"imageWidth\":%" PRId32",\n",
				       ti->m_dx);
			sb->safePrintf("\t\t\"origImageHeight\":%" PRId32",\n",
				       ti->m_origDY);
			sb->safePrintf("\t\t\"origImageWidth\":%" PRId32",\n",
				       ti->m_origDX);
			sb->safePrintf("\t\t\"imageUrl\":\"");
			sb->jsonEncode(ti->getUrl());
			sb->safePrintf("\",\n");
		}
	}

	bool isWide = false;
	int32_t newdx = 0;

	// print image for widget
	if ( ( si->m_format == FORMAT_WIDGET_IFRAME ||
	       si->m_format == FORMAT_WIDGET_AJAX ||
	       si->m_format == FORMAT_WIDGET_APPEND ) ) {

		int32_t widgetWidth = hr->getLong("widgetwidth",200);

		// prevent coring
		if ( widgetWidth < 1 ) widgetWidth = 1;

		// each search result in widget has a div around it
		sb->safePrintf("<div "
			       "class=result "
			       // we need the docid and score of last result
			       // when we append new results to the end
			       // of the widget for infinite scrolling
			       // using the scripts in PageBasic.cpp
			       "docid=%" PRId64" "
			       "score=%f " // double
			       "style=\""
			       "width:%" PRId32"px;"
			       "min-height:%" PRId32"px;"
			       "height:%" PRId32"px;"
			       "padding:%" PRId32"px;"
			       "position:relative;"
			       // summary overflows w/o this!
			       "overflow-y:hidden;"
			       "overflow-x:hidden;"
			       "\""
			       ">"
			       , mr->m_docId
			       // this is a double now. this won't work
			       // for streaming...
			       , msg40->m_msg3a.getScores()[ix]
			       // subtract 8 for scrollbar on right
			       , widgetWidth - 2*8 - 8 // padding is 8px
			       , (int32_t)RESULT_HEIGHT
			       , (int32_t)RESULT_HEIGHT
			       , (int32_t)PADDING
			       );
		if ( mr->ptr_imgData ) {
			ThumbnailArray *ta = (ThumbnailArray *)mr->ptr_imgData;
			ThumbnailInfo *ti = ta->getThumbnailInfo(0);
			// account for scrollbar on the right
			int32_t maxWidth = widgetWidth - (int32_t)SCROLLBAR_WIDTH;
			int32_t maxHeight = (int32_t)RESULT_HEIGHT;
			// false = do not print <a href> link on image
			ti->printThumbnailInHtml ( sb , 
						   maxWidth ,
						   maxHeight , 
						   false , // add <a href>
						   &newdx );
		}

		sb->safePrintf ( "<a "
				 "target=_blank "
				 "style=\"text-decoration:none;"
				 // don't let scroll bar obscure text
				 "margin-right:%" PRId32"px;"
				 ,(int32_t)SCROLLBAR_WIDTH
				 );

		// if thumbnail is wide enough put text on top of it, otherwise
		// image is to the left and text is to the right of image
		if ( newdx > .5 * widgetWidth ) {
			isWide = true;
			sb->safePrintf("position:absolute;"
				       "bottom:%" PRId32";"
				       "left:%" PRId32";"
				       , (int32_t) PADDING 
				       , (int32_t) PADDING 
				       );
		}
		// to align the text verticall we gotta make a textbox div
		// otherwise it wraps below image! mdw
		//else
		//	sb->safePrintf("vertical-align:middle;");
		else
			sb->safePrintf("position:absolute;"
				       "bottom:%" PRId32";"
				       "left:%" PRId32";"
				       , (int32_t) PADDING
				       , (int32_t) PADDING + newdx + 10 );

		// close the style and begin the url
		sb->safePrintf( "\" "
				"href=\"" 
				 );

		// truncate off -diffbotxyz%" PRId32"
		int32_t newLen = urlLen;
		// print the url in the href tag
		sb->safeMemcpy ( url , newLen ); 
		// then finish the a href tag and start a bold for title
		sb->safePrintf ( "\">");
		
		sb->safePrintf("<b style=\""
			       "text-decoration:none;"
			       "font-size: 15px;"
			       "font-weight:bold;"
			       "background-color:rgba(0,0,0,.5);"
			       "color:white;"
			       "font-family:arial;"
			       "text-shadow: 2px 2px 0 #000 "
			       ",-2px -2px 0 #000 "
			       ",-2px  2px 0 #000 "
			       ", 2px -2px 0 #000 "
			       ", 2px -2px 0 #000 "
			       ", 0px -2px 0 #000 "
			       ",  0px 2px 0 #000 "
			       ", -2px 0px 0 #000 "
			       ",  2px 0px 0 #000 "
			       ";"
			       "\">");
		// then title over image
	}

	// only do link here if we have no thumbnail so no bg image
	if ( (si->m_format == FORMAT_WIDGET_IFRAME ||
	      si->m_format == FORMAT_WIDGET_APPEND ||
	      si->m_format == FORMAT_WIDGET_AJAX   ) &&
	     ! mr->ptr_imgData ) {
		sb->safePrintf ( "<a style=text-decoration:none;"
				 "color:white; "
				 "href=" );
		// truncate off -diffbotxyz%" PRId32"
		int32_t newLen = urlLen;
		// print the url in the href tag
		sb->safeMemcpy ( url , newLen ); 
		// then finish the a href tag and start a bold for title
		sb->safePrintf ( ">");
	}


	// the a href tag
	if ( si->m_format == FORMAT_HTML ) sb->safePrintf ( "\n\n" );

	// then if it is banned 
	if ( mr->m_isBanned && si->m_format == FORMAT_HTML )
		sb->safePrintf("<font color=red><b>BANNED</b></font> ");


	///////
	//
	// PRINT THE TITLE
	//
	///////

	// the a href tag
	if ( si->m_format == FORMAT_HTML ) {
		sb->safePrintf ( "<a href=\"" );
		// truncate off -diffbotxyz%" PRId32"
		int32_t newLen = urlLen;
		// print the url in the href tag
		sb->safeMemcpy ( url , newLen ); 
		// then finish the a href tag and start a bold for title
		sb->safePrintf ( "\">");
	}

	const char  *str = nullptr;
	int32_t strLen = 0;

	// . then the title  (should be NULL terminated)
	// . the title can be NULL
	// . highlight it first
	// . the title itself should not have any tags in it!
	str = mr->ptr_tbuf;
	strLen = mr->size_tbuf - 1;
	if (!str || strLen < 0) {
		strLen = 0;
	}

	std::string preferredResultLang = si->getPreferredResultLanguage();
	std::string overriddenTitle;
	// override title
	if (strLen == 0 && mr->m_indexCode == EDOCDISALLOWEDROOT) {
		overriddenTitle = g_robotsBlockedResultOverride.getTitle(preferredResultLang, uu);
		str = overriddenTitle.c_str();
		strLen = overriddenTitle.length();
	}

	const char *frontTag = "<font style=\"color:black;background-color:yellow\">" ;
	const char *backTag = "</font>";
	if ( si->m_format == FORMAT_XML ) {
		frontTag = "<b>";
		backTag  = "</b>";
	}
	if ( si->m_format == FORMAT_WIDGET_IFRAME || 
	     si->m_format == FORMAT_WIDGET_APPEND ||
	     si->m_format == FORMAT_WIDGET_AJAX ) {
		frontTag = "<font style=\"background-color:yellow\">" ;
	}
	int32_t cols = si->m_summaryMaxWidth;

	// url encode title
	StackBuf<> tmpTitle;
	if ( str && strLen ) {
		tmpTitle.htmlEncode(str, strLen, false);
	}

	StackBuf<> hb;
	if ( str && strLen && si->m_doQueryHighlighting ) {
		hi.set ( &hb, tmpTitle.getBufStart(), tmpTitle.length(), &si->m_hqq, frontTag, backTag);

		// reassign!
		str = hb.getBufStart();
		strLen = hb.length();
	}

	// . use "UNTITLED" if no title
	// . msg20 should supply the dmoz title if it can
	if ( strLen == 0 &&  si->m_format != FORMAT_XML &&  si->m_format != FORMAT_JSON ) {
		str = "<i>UNTITLED</i>";
		strLen = strlen(str);
	}

	if ( str && strLen &&
	     ( si->m_format == FORMAT_HTML ||
	       si->m_format == FORMAT_WIDGET_IFRAME ||
	       si->m_format == FORMAT_WIDGET_APPEND ||
	       si->m_format == FORMAT_WIDGET_AJAX ) 
	     ) {
		// determine if TiTle wraps, if it does add a <br> count for each wrap
		if ( !sb->brify( str, strLen, cols ) ) {
			return false;
		}
	}

	// close up the title tag
	if ( si->m_format == FORMAT_XML ) {
		sb->safePrintf("\t\t<title><![CDATA[");
		if ( str ) {
			cdataEncode(sb, str);
		}
		sb->safePrintf("]]></title>\n");
	}

	if ( si->m_format == FORMAT_JSON ) {
		sb->safePrintf("\t\t\"title\":\"");
		if ( str ) {
			sb->jsonEncode(str);
		}
		sb->safePrintf("\",\n");
	}


	if ( si->m_format == FORMAT_HTML ) 
		sb->safePrintf ("</a><br>\n" ) ;


	// close the title tag stuf
	if ( si->m_format == FORMAT_WIDGET_IFRAME ||
	     si->m_format == FORMAT_WIDGET_APPEND ||
	     si->m_format == FORMAT_WIDGET_AJAX ) 
		sb->safePrintf("</b></a>\n");

	//
	// print <h1> tag contents. hack for client.
	//
	char *hp = mr->ptr_htag;
	char *hpend = hp + mr->size_htag;
	for ( ; hp && hp < hpend ; ) {
		if ( si->m_format == FORMAT_XML ) {
			sb->safePrintf("\t\t<h1Tag><![CDATA[");
			cdataEncode(sb, hp);
			sb->safePrintf("]]></h1Tag>\n");
		}
		if ( si->m_format == FORMAT_JSON ) {
			sb->safePrintf("\t\t\"h1Tag\":\"");
			sb->jsonEncode(hp);
			sb->safePrintf("\",\n");
		}
		// it is a \0 separated list of headers generated from XmlDoc::getHeaderTagBuf()
		hp += strlen(hp) + 1;
	}

	// print the [cached] link?
	bool printCached;
	if ( mr->m_contentLen <= 0 )
		printCached = false; //nothing to show
	else if ( isAdmin )
		printCached = true; //admin can bypass noarchive tag
	else if( mr->m_noArchive )
		printCached = false; //page doesn't want to be archived. honour that.
	else
		printCached = true;

	/////
	//
	// print content type after title
	//
	/////
	unsigned char ctype = mr->m_contentType;
	const char *cs = g_contentTypeStrings[ctype];

	if ( si->m_format == FORMAT_XML )
		sb->safePrintf("\t\t<contentType><![CDATA[%s]]></contentType>\n", cs);

	if ( si->m_format == FORMAT_JSON )
		sb->safePrintf("\t\t\"contentType\":\"%s\",\n",cs);

	if ( si->m_format == FORMAT_HTML && ctype != CT_HTML && ctype != CT_UNKNOWN ) {
		sb->safePrintf(" <b><font style=color:white;background-color:maroon;>");
		const char *p = cs;
		for ( ; *p ; p++ ) {
			char c = to_upper_a(*p);
			sb->pushChar(c);
		}
		sb->safePrintf("</font></b> &nbsp;");
	}
	

	////////////
	//
	// print the summary
	//
	////////////

	// do the normal summary
	str = mr->ptr_displaySum;

	// sometimes the summary is longer than requested because for
	// summary deduping purposes (see "pss" parm in Parms.cpp) we do not
	// get it as short as requested. so use mr->m_sumPrintSize here
	// not mr->size_sum
	strLen = mr->size_displaySum - 1;

	// this includes the terminating \0 or \0\0 so back up
	if ( strLen < 0 ) {
		strLen  = 0;
	}

	// override summary
	std::string overriddenSummary;
	if (strLen == 0 && mr->m_indexCode == EDOCDISALLOWEDROOT) {
		overriddenSummary = g_robotsBlockedResultOverride.getSummary(preferredResultLang, uu);
		str = overriddenSummary.c_str();
		strLen = overriddenSummary.length();
	}

	bool printSummary = true;

	// do not print summaries for widgets by default unless overridden with &summary=1
	int32_t defSum = 0;

	// if no image then default the summary to on
	if ( ! mr->ptr_imgData ) {
		defSum = 1;
	}

	if ( (si->m_format == FORMAT_WIDGET_IFRAME ||
	      si->m_format == FORMAT_WIDGET_APPEND ||
	      si->m_format == FORMAT_WIDGET_AJAX ) && 
	     hr->getLong("summaries",defSum) == 0 ) {
		printSummary = false;
	}

	if ( printSummary &&
	     (si->m_format == FORMAT_WIDGET_IFRAME ||
	      si->m_format == FORMAT_WIDGET_APPEND ||
	      si->m_format == FORMAT_WIDGET_AJAX ) ) {
		int32_t sumLen = strLen;
		if ( sumLen > 150 ) sumLen = 150;
		if ( sumLen ) {
			sb->safePrintf("<br>");
			sb->safeTruncateEllipsis ( str , sumLen );
		}
	}

	if ( si->m_format == FORMAT_HTML ) {
		if ( printSummary ) {
			sb->brify( str, strLen, cols );
		}

		// new line if not xml. even summary is empty we need it too like
		// when showing xml docs - MDW 9/28/2014
		sb->safePrintf( "<br>\n" );
	}
	else
	if ( si->m_format == FORMAT_XML ) {
		sb->safePrintf( "\t\t<sum><![CDATA[" );
		cdataEncode(sb, str);
		sb->safePrintf( "]]></sum>\n" );
	}
	else
	if ( si->m_format == FORMAT_JSON ) {
		sb->safePrintf( "\t\t\"sum\":\"" );
		sb->jsonEncode( str );
		sb->safePrintf( "\",\n" );
	}

	/////////
	// 
	// meta tag values for &dt=keywords ...
	//
	/////////
	if ( mr->ptr_dbuf && mr->size_dbuf > 1 )
		printMetaContent ( msg40 , ix,st,sb);

	////////////
	//
	// print the URL
	//
	////////////

	StackBuf<> tmpBuf;
	char* displayUrl = Url::getDisplayUrl(url, &tmpBuf);
	uint32_t displayUrlLen = tmpBuf.length();

	// hack off the http:// if any for displaying it on screen
	if ( displayUrlLen > 8 && strncmp ( displayUrl , "http://" , 7 )==0 ) {
		displayUrl += 7; displayUrlLen -= 7; }

	// . remove trailing /
	// . only remove from root urls in case user cuts and 
	//   pastes it for link: search
	if ( displayUrl [ displayUrlLen - 1 ] == '/' ) {
		// see if any other slash before us
		int32_t j;
		for ( j = displayUrlLen - 2 ; j >= 0 ; j-- )
			if ( displayUrl[j] == '/' ) break;
		// if there wasn't, we must have been a root url
		// so hack off the last slash
		if ( j < 0 ) displayUrlLen--;
	}

	if ( si->m_format == FORMAT_HTML ) {
		sb->safePrintf ("<font color=gray>" );
		//sb->htmlEncode ( url , strlen(url) , false );
		// 20 for the date after it
		sb->safeTruncateEllipsis ( displayUrl , 50 ); // cols - 30 );
		// turn off the color
		sb->safePrintf ( "</font>\n" );
	}

	// print url for widgets now
	if ( (si->m_format == FORMAT_WIDGET_IFRAME ||
	      si->m_format == FORMAT_WIDGET_APPEND ||
	      si->m_format == FORMAT_WIDGET_AJAX ) ) {
		//sb->safePrintf ("<br><font color=gray size=-1>" );
		// print url for widgets in top left if we have a wide image
		// otherwise it gets truncated below the title for some reason
		if ( isWide )
			sb->safePrintf ("<br><font color=white size=-1 "
					"style=position:absolute;left:10px;"
					"top:10px;background-color:black;>" );
		else if ( mr->ptr_imgData )
			sb->safePrintf ("<br><font color=gray size=-1 "
					"style=position:absolute;left:%" PRId32"px;"
					"top:10px;>"
				       , (int32_t) PADDING + newdx + 10 );
		else
			sb->safePrintf ("<br><font color=gray size=-1>");
		// print the url now, truncated to 50 chars
		sb->safeTruncateEllipsis ( url , 50 ); // cols - 30 );
		sb->safePrintf ( "</font>\n" );
	}


	if ( si->m_format == FORMAT_XML ) {
		sb->safePrintf("\t\t<url><![CDATA[");
		sb->safeMemcpy ( displayUrl , displayUrlLen );
		sb->safePrintf("]]></url>\n");
	}
	if ( si->m_format == FORMAT_JSON ) {
		sb->safePrintf("\t\t\"url\":\"");
		sb->jsonEncode ( displayUrl , displayUrlLen );
		sb->safePrintf("\",\n");
	}

	// now the last spidered date of the document
	time_t ts = mr->m_lastSpidered;
	if(si->m_format == FORMAT_HTML && ts>0) {
		sb->safePrintf(" - indexed: ");
		time_t now = getTime();
		printTimeAgo(sb, now-ts, now, false);
	}

	// the date it was last modified
	ts = mr->m_lastModified;
	if(si->m_format == FORMAT_HTML && ts>0) {
		sb->safePrintf(" - modified: ");
		time_t now = getTime();
		printTimeAgo(sb, now-ts, now, false);
	}


	//
	// more xml stuff
	//
	if ( si->m_format == FORMAT_XML ) {
		// doc size in Kilobytes
		sb->safePrintf ("\t\t<size><![CDATA[%4.0fk]]></size>\n", (float)mr->m_contentLen/1024.0);
		sb->safePrintf ( "\t\t<sizeInBytes>%" PRId32"</sizeInBytes>\n", mr->m_contentLen);
		// . docId for possible cached link
		// . might have merged a bunch together
		sb->safePrintf("\t\t<docId>%" PRId64"</docId>\n",mr->m_docId );
		sb->safePrintf("\t\t<docScore>%f</docScore>\n",docScore);

		// . show the site root
		// . for hompages.com/users/fred/mypage.html this will be
		//   homepages.com/users/fred/
		// . for www.xyz.edu/~foo/burp/ this will be
		//   www.xyz.edu/~foo/ etc.
		// seems like this isn't the way to do it, cuz Tagdb.cpp
		// adds the "site" tag itself and we do not always have it
		// in the XmlDoc::ptr_tagRec... so do it this way:
		const char *site    = mr->ptr_site;
		int32_t siteLen = mr->size_site-1;
		//char *site=uu.getSite( &siteLen , si->m_coll, false, tagRec);
		sb->safePrintf("\t\t<site><![CDATA[");
		if ( site && siteLen > 0 ) sb->safeMemcpy ( site , siteLen );
		sb->safePrintf("]]></site>\n");
		//int32_t sh = hash32 ( site , siteLen );
		//sb->safePrintf ("\t\t<siteHash32>%" PRIu32"</siteHash32>\n",sh);
		//int32_t dh = uu.getDomainHash32 ();
		//sb->safePrintf ("\t\t<domainHash32>%" PRIu32"</domainHash32>\n",dh);
		// spider date
		sb->safePrintf ( "\t\t<spidered>%" PRIu32"</spidered>\n", (uint32_t)mr->m_lastSpidered);
		// backwards compatibility for buzz
		sb->safePrintf ( "\t\t<firstIndexedDateUTC>%" PRIu32 "</firstIndexedDateUTC>\n", (uint32_t)mr->m_firstIndexedDate);
		sb->safePrintf( "\t\t<contentHash32>%" PRIu32 "</contentHash32>\n", (uint32_t)mr->m_contentHash32);
		// pub date
		int32_t datedbDate = mr->m_datedbDate;
		// show the datedb date as "<pubDate>" for now
		if ( datedbDate != -1 )
			sb->safePrintf ( "\t\t<pubdate>%" PRIu32"</pubdate>\n", (uint32_t)datedbDate);
	}

	if ( si->m_format == FORMAT_JSON ) {
		// doc size in Kilobytes
		sb->safePrintf ( "\t\t\"size\":\"%4.0fk\",\n", (float)mr->m_contentLen/1024.0);
		sb->safePrintf ( "\t\t\"sizeInBytes\":%" PRId32 ",\n", mr->m_contentLen);
		// . docId for possible cached link
		// . might have merged a bunch together
		sb->safePrintf("\t\t\"docId\":%" PRId64",\n", mr->m_docId );
		sb->safePrintf("\t\t\"docScore\":%f,\n", docScore);
		sb->safePrintf("\t\t\"flags\":%u,\n", docFlags);
		sb->safePrintf("\t\t\"cacheAvailable\":%s,\n", printCached?"true":"false");
		sb->safePrintf("\t\t\"isAdult\":%s,\n", mr->m_isAdult?"true":"false");

		// . show the site root
		// . for hompages.com/users/fred/mypage.html this will be
		//   homepages.com/users/fred/
		// . for www.xyz.edu/~foo/burp/ this will be
		//   www.xyz.edu/~foo/ etc.
		// seems like this isn't the way to do it, cuz Tagdb.cpp
		// adds the "site" tag itself and we do not always have it
		// in the XmlDoc::ptr_tagRec... so do it this way:
		const char *site = mr->ptr_site;
		int32_t siteLen = mr->size_site-1;
		//char *site=uu.getSite( &siteLen , si->m_coll, false, tagRec);
		sb->safePrintf("\t\t\"site\":\"");
		if ( site && siteLen > 0 ) sb->safeMemcpy ( site , siteLen );
		sb->safePrintf("\",\n");
		// spider date
		sb->safePrintf ( "\t\t\"spidered\":%" PRIu32 ",\n", (uint32_t)mr->m_lastSpidered);
		// backwards compatibility for buzz
		sb->safePrintf ( "\t\t\"firstIndexedDateUTC\":%" PRIu32 ",\n", (uint32_t) mr->m_firstIndexedDate);
		sb->safePrintf( "\t\t\"contentHash32\":%" PRIu32 ",\n", (uint32_t)mr->m_contentHash32);
		// pub date
		int32_t datedbDate = mr->m_datedbDate;
		// show the datedb date as "<pubDate>" for now
		if ( datedbDate != -1 )
			sb->safePrintf ( "\t\t\"pubdate\":%" PRIu32 ",\n", (uint32_t)datedbDate);
	}


	// . we also store the outlinks in a linkInfo structure
	// . we can call LinkInfo::set ( Links *outlinks ) to set it
	//   in the msg20
	LinkInfo *outlinks = (LinkInfo *)mr->ptr_outlinks;
	// NULLify if empty
	if ( mr->size_outlinks <= 0 ) outlinks = NULL;
	// only for xml for now
	if ( si->m_format == FORMAT_HTML ) outlinks = NULL;
	Inlink *k = NULL;
	// do we need absScore2 for outlinks?
	while ( outlinks &&
		(k =outlinks->getNextInlink(k))) 
		// print it out
		sb->safePrintf("\t\t<outlink docId=\"%" PRId64"\" hostId=\"%" PRIu32"\" indexed=\"%" PRId32"\" pubdate=\"%" PRId32"\" ",
			k->m_docId ,
			(uint32_t)k->m_ip,//hostHash, but use ip for now
			(int32_t)k->m_firstIndexedDate ,
			(int32_t)k->m_datedbDate );

	if ( si->m_format == FORMAT_XML ) {
		// result
		sb->safePrintf("\t\t<language><![CDATA[%s]]></language>\n", getLanguageString(mr->m_language));
		sb->safePrintf("\t\t<langAbbr>%s</langAbbr>\n", getLanguageAbbr(mr->m_language));
	}

	if ( si->m_format == FORMAT_JSON ) {
		// result
		sb->safePrintf("\t\t\"language\":\"%s\",\n", getLanguageString(mr->m_language));
		sb->safePrintf("\t\t\"langAbbr\":\"%s\",\n", getLanguageAbbr(mr->m_language));
	}

	//
	// end more xml stuff
	//

	
	if ( si->m_format == FORMAT_HTML ) {
		int32_t lang = mr->m_language;
		if ( lang ) sb->safePrintf(" - %s",getLanguageString(lang));

		sb->safePrintf("<br>\n");
	}

	// if searching multiple collections  - federated search
	CollectionRec *scr = g_collectiondb.getRec ( mr->m_collnum );
	const char *coll = "UNKNOWN";
	if ( scr ) coll = scr->m_coll;

	if ( si->m_format == FORMAT_HTML && printCached ) {
		sb->safePrintf ( "<a href=\"/get?q=%s&qlang=%s&c=%s&d=%" PRId64 "&cnsp=0\">cached</a>\n",
			st->m_qesb.getBufStart() ,
			si->m_defaultSortLang,		// "qlang" parm
			coll ,
			mr->m_docId );
	}

	// unhide the divs on click
	int32_t placeHolder = -1;
	int32_t placeHolderLen = 0;
	if ( si->m_format == FORMAT_HTML && si->m_getDocIdScoringInfo ) {
		// place holder for backlink table link
		placeHolder = sb->length();
		sb->safePrintf (" - <a onclick="
			       "\""
			       "var e = document.getElementById('bl%" PRId32"');"
			       "if ( e.style.display == 'none' ){"
			       "e.style.display = '';"
			       "}"
			       "else {"
			       "e.style.display = 'none';"
			       "}"
			       "\""
			       " "
			       "style="
			       "cursor:hand;"
			       "cursor:pointer;"
			       "color:blue;>"
			       "<u>00000 backlinks</u>"
			       "</a>\n"
			       , ix 
			       );
		placeHolderLen = sb->length() - placeHolder;

		// unhide the scoring table on click
		sb->safePrintf (" - <a onclick="
			       "\""
			       "var e = document.getElementById('sc%" PRId32"');"
			       "if ( e.style.display == 'none' ){"
			       "e.style.display = '';"
			       "}"
			       "else {"
			       "e.style.display = 'none';"
			       "}"
			       "\""
			       " "
			       "style="
			       "cursor:hand;"
			       "cursor:pointer;"
			       "color:blue;>"
			       "scoring"
			       "</a>\n"
			       ,ix
			       );
	}

	if ( si->m_format == FORMAT_HTML ) {
		// reindex
		sb->safePrintf(" - <a style=color:blue; href=\"/addurl?urls=");
		urlEncode(sb, url, strlen(url), false);
		uint64_t rand64 = gettimeofdayInMilliseconds();
		sb->safePrintf("&c=%s&rand64=%" PRIu64"\">respider</a>\n", coll,rand64);

		sb->safePrintf (" - "
				"<a style=color:blue; "
				"href=\"/search?sb=1&c=%s&"
				"q=gbfieldmatch%%3AgbssUrl%%3A"
				, coll 
				);
		// do not include ending \0
		urlEncode(sb, mr->ptr_ubuf, mr->size_ubuf-1, false);
		sb->safePrintf("\">spider info</a>\n");


		//
		// show rainbow sections link
		//

		sb->safePrintf ( " - <a style=color:blue; href=\""
				 "/get?"
				 "page=4&"	// show rainbow sections
				 "q=%s&"
				 "qlang=%s&"
				 "c=%s&"
				 "d=%" PRId64"&"
				 "cnsp=0\">"
				 "sections</a>\n", 
				 st->m_qesb.getBufStart() , 
				 si->m_defaultSortLang,	// "qlang" parm
				 coll , 
				 mr->m_docId ); 

		sb->safePrintf ( " - <a style=color:blue; href=\""
				 "/get?"
				 "page=1&"	// show rainbow sections
				 //"q=%s&"
				 //"qlang=%s&"
				 "c=%s&"
				 "d=%" PRId64"&"
				 "cnsp=0\">"
				 "page info</a>\n", 
				 //st->m_qe , 
				 // "qlang" parm
				 //si->m_defaultSortLang,
				 coll , 
				 mr->m_docId ); 

		sb->safePrintf ( " - <a style=color:blue; href=\""
				 "/get?"
				 "page=5&"	// show rainbow sections
				 //"q=%s&"
				 //"qlang=%s&"
				 "c=%s&"
				 "d=%" PRId64"&"
				 "cnsp=0\">"
				 "term info</a>\n", 
				 //st->m_qe , 
				 // "qlang" parm
				 //si->m_defaultSortLang,
				 coll , 
				 mr->m_docId ); 

		char ipbuf1[16];
		char ipbuf2[16];
		sb->safePrintf(//"<br>"
			      " - <a style=color:blue; href=\"/search?"
			      "c=%s&sc=1&dr=0&q=ip:%s&"
			      "n=100&usecache=0\">%s</a>\n",
			      coll,iptoa(mr->m_ip,ipbuf1), iptoa(mr->m_ip,ipbuf2) );
		// ip domain link
		unsigned char *us = (unsigned char *)&mr->m_ip;//urlip;
		sb->safePrintf (" - <a style=color:blue; "
				"href=\"/search?c=%s&sc=1&dr=0&n=100&"
				"q=ip:%" PRId32".%" PRId32".%" PRId32"&"
				"usecache=0\">%" PRId32".%" PRId32".%" PRId32"</a>\n",
				coll,
				(int32_t)us[0],(int32_t)us[1],(int32_t)us[2],
				(int32_t)us[0],(int32_t)us[1],(int32_t)us[2]);
	}

	char dbuf [ MAX_URL_LEN ];
	int32_t dlen = uu.getDomainLen();

	if ( si->m_format == FORMAT_HTML ) {
		memcpy ( dbuf , uu.getDomain() , dlen );
		dbuf [ dlen ] = '\0';
		// newspaperarchive urls have no domain
		if ( dlen == 0 ) {
			dlen = uu.getHostLen();
			memcpy ( dbuf , uu.getHost() , dlen );
			dbuf [ dlen ] = '\0';
		}

		// admin always gets the site: option so he can ban
		sb->safePrintf (" - "
			       " <a style=color:blue; href=\"/search?"
			       "q=site%%3A%s&sc=0&c=%s\">"
			       "domain</a>\n" ,
				dbuf ,
				coll );
	}


	if ( si->m_format == FORMAT_HTML && si->m_doSiteClustering ) {
		char hbuf [ MAX_URL_LEN ];
		int32_t hlen = uu.getHostLen();
		memcpy ( hbuf , uu.getHost() , hlen );
		hbuf [ hlen ] = '\0';

		// make the cgi parm to add to the original url
		StackBuf<512> qq;
		qq.safePrintf("q=");
		urlEncode(&qq, "site:");
		urlEncode(&qq, hbuf);
		urlEncode(&qq, " | ");
		qq.safeStrcpy(st->m_qesb.getBufStart());
		qq.nullTerm();
		// get the original url and add/replace in query
		StackBuf<512> newUrl;
		replaceParm ( qq.getBufStart() , &newUrl , hr );
		// put show more results from this site link
		sb->safePrintf (" - <nobr><a href=\"%s\">"
			       "more from this site</a></nobr>"
				, newUrl.getBufStart()
				);
	}


	if (si->m_format == FORMAT_HTML && isAdmin ){
		const char *un = "";
		int32_t  banVal = 1;
		if ( mr->m_isBanned ) {
			un = "UN";
			banVal = 0;
		}
		// don't put on a separate line because then it is too
		// easy to mis-click on it
		sb->safePrintf(//"<br>"
			       " - "
			      " <a style=color:green; href=\"/admin/tagdb?"
			      "user=admin&"
			      "tagtype0=manualban&"
			      "tagdata0=%" PRId32"&"
			      "u=%s&c=%s\">"
			      "<nobr>%sBAN %s"
			      "</nobr></a>\n"
			      , banVal
			      , dbuf
			      , coll
			      , un
			      , dbuf );
		dlen = uu.getHostLen();
		memcpy ( dbuf , uu.getHost() , dlen );
		dbuf [ dlen ] = '\0';
		sb->safePrintf(" - "
			      " <a style=color:green; href=\"/admin/tagdb?"
			      "user=admin&"
			      "tagtype0=manualban&"
			      "tagdata0=%" PRId32"&"
			      "u=%s&c=%s\">"
			      "<nobr>%sBAN %s</nobr></a>\n"
			      , banVal
			      , dbuf
			      , coll
			      , un
			      , dbuf
			      );
	}

	if ( mr->size_metadataBuf && si->m_format == FORMAT_JSON) {
		sb->safePrintf("\t\t\"metadata\":[");
		//sb->safeMemcpy(mr->ptr_metadataBuf, mr->size_metadataBuf);
		sb->safeStrcpy(mr->ptr_metadataBuf);
		// without this \n we seem to lose our ] i guess it gets
		// backed up over
		sb->safePrintf("],\n");
	}


	if ( mr->size_metadataBuf && si->m_format == FORMAT_HTML) {
		sb->safePrintf("<br>");

		Json md;
		JsonItem *ji = md.parseJsonStringIntoJsonItems(mr->ptr_metadataBuf);
		StackBuf<1024> nameBuf;
		for ( ; ji ; ji = ji->m_next ) {
			if(ji->isInArray()) continue;
			if(ji->m_type == JT_ARRAY) continue;
			ji->getCompoundName ( nameBuf ) ;
			if(nameBuf.length() == 0) {
				continue;
			}
			//nameBuf.replaceChar('-', '_');
			nameBuf.nullTerm();

			int32_t valLen;
			const char* valBuf = ji->getValueAsString(&valLen);
			StackBuf<1024> queryBuf;
			// log("compound name is %s %d %d",nameBuf.getBufStart(),
			// nameBuf.length(), valLen);

			queryBuf.safePrintf("/search?q=%s:%%22",nameBuf.getBufStart());
			urlEncode(&queryBuf, valBuf, valLen);
			queryBuf.safePrintf("%%22&c=%s",coll);
			queryBuf.nullTerm();
			sb->safePrintf(" - <a href=\"%s\">%s:\"", queryBuf.getBufStart(),
						   nameBuf.getBufStart());
			sb->safeMemcpy(valBuf, valLen);
			sb->safeStrcpy("\"</a>");
		}
	}


	// end serp div
	if ( si->m_format == FORMAT_WIDGET_IFRAME ||
	     si->m_format == FORMAT_WIDGET_APPEND ||
	     si->m_format == FORMAT_WIDGET_AJAX )
		sb->safePrintf("</div><hr>");
	

	if ( si->m_format == FORMAT_HTML )
		sb->safePrintf ( "<br><br>\n");

	// search result spacer
	if ( si->m_format == FORMAT_WIDGET_IFRAME ||
	     si->m_format == FORMAT_WIDGET_APPEND ||
	     si->m_format == FORMAT_WIDGET_AJAX   )
		sb->safePrintf("<div style=line-height:%" PRId32"px;><br></div>",
			       (int32_t)SERP_SPACER);


	// inc it
	*numPrintedSoFar = *numPrintedSoFar + 1;

	// done?
	DocIdScore *dp = msg40->getScoreInfo(ix);
	if ( ! dp ) {
		if ( si->m_format == FORMAT_XML ) 
			sb->safePrintf ("\t</result>\n\n");
		if ( si->m_format == FORMAT_JSON ) {
			// remove last ,\n
			sb->m_length -= 2;
			sb->safePrintf ("\n\t}\n");
		}
		// wtf?
		//g_process.shutdownAbort(true);
		// at least close up the table
		if ( si->m_format != FORMAT_HTML ) return true;

		sb->safePrintf("</table>\n");
		//blank out the "xxxxx backlinks" part
		memset(sb->getBufStart() + placeHolder, ' ', placeHolderLen);

		return true;
	}


	//
	// scoring info tables
	//

	int32_t nr = dp->m_numRequiredTerms;
	if ( nr == 1 ) nr = 0;
	// print breakout tables here for distance matrix
	// final score calc
	StackBuf<1024> ft;

	// put in a hidden div so you can unhide it
	if ( si->m_format == FORMAT_HTML )
		sb->safePrintf("<div id=bl%" PRId32" style=display:none;>\n", ix );

	// print xml and html inlinks
	int32_t numInlinks = 0;
	printInlinkText ( sb , mr , si , &numInlinks );


	if ( si->m_format == FORMAT_HTML ) {
		sb->safePrintf("</div>");
		sb->safePrintf("<div id=sc%" PRId32" style=display:none;>\n", ix );
	}


	// if pair changes then display the sum
	int32_t lastTermNum1 = -1;
	int32_t lastTermNum2 = -1;

	float minScore = -1;

	// display all the PairScores
	for ( int32_t i = 0 ; i < dp->m_numPairs ; i++ ) {
		float totalPairScore = 0.0;
		// print all the top winners for this pair
		PairScore *fps = &dp->m_pairScores[i];
		// if same combo as last time skip
		if ( fps->m_qtermNum1 == lastTermNum1 &&
		     fps->m_qtermNum2 == lastTermNum2 )
			continue;
		lastTermNum1 = fps->m_qtermNum1;
		lastTermNum2 = fps->m_qtermNum2;
		bool firstTime = true;
		// print all pairs for this combo
		for ( int32_t j = i ; j < dp->m_numPairs ; j++ ) {
			// get it
			PairScore *ps = &dp->m_pairScores[j];
			// stop if different pair now
			if ( ps->m_qtermNum1 != fps->m_qtermNum1 ) break;
			if ( ps->m_qtermNum2 != fps->m_qtermNum2 ) break;
			// skip if 0. neighborhood terms have weight of 0 now
			if ( almostEqualFloat(ps->m_finalScore, 0.0) ) continue;
			// first time?
			if ( firstTime && si->m_format == FORMAT_HTML ) {
				const Query *q = &si->m_q;
				printTermPairs ( sb , q , ps );
				printScoresHeader ( sb );
				firstTime = false;
			}
			// print it
			printPairScore ( sb , si , ps , mr );

			// add it up
			totalPairScore += ps->m_finalScore;
		}
		if ( ft.length() ) ft.safePrintf(" , ");
		ft.safePrintf("%f",totalPairScore);
		// min?
		if ( minScore < 0.0 || totalPairScore < minScore )
			minScore = totalPairScore;
		// we need to set "ft" for xml stuff below
		if ( si->m_format != FORMAT_HTML ) continue;

		sb->safePrintf("<tr><td><b>%.04f</b></td><td colspan=20>total of above scores</td></tr>", totalPairScore);
		// close table from printScoresHeader
		if ( ! firstTime ) sb->safePrintf("</table><br>");
	}

	int32_t lastTermNum = -1;

	int32_t numSingles = dp->m_numSingles;
	// do not print this if we got pairs
	if ( dp->m_numPairs ) numSingles = 0;

	for ( int32_t i = 0 ; i < numSingles ; i++ ) {
		float totalSingleScore = 0.0;
		// print all the top winners for this single
		SingleScore *fss = &dp->m_singleScores[i];
		// if same combo as last time skip
		if ( fss->m_qtermNum == lastTermNum ) continue;
		// do not reprint for this query term num
		lastTermNum = fss->m_qtermNum;
		bool firstTime = true;
		// print all singles for this combo
		for ( int32_t j = i ; j < dp->m_numSingles ; j++ ) {
			// get it
			SingleScore *ss = &dp->m_singleScores[j];
			// stop if different single now
			if ( ss->m_qtermNum != fss->m_qtermNum ) break;
			// skip if 0. skip neighborhoods i guess
			if ( almostEqualFloat(ss->m_finalScore, 0.0) ) continue;
			// first time?
			if ( firstTime && si->m_format == FORMAT_HTML ) {
				const Query *q = &si->m_q;
				printSingleTerm ( sb , q , ss );
				printScoresHeader ( sb );
				firstTime = false;
			}

			// print it
			printSingleScore ( sb , si , ss , mr );
			if(si->m_format == FORMAT_JSON && j+1 < dp->m_numSingles)
				sb->safePrintf(",\n");
			else
				sb->safePrintf("\n");

			// add up
			totalSingleScore += ss->m_finalScore;
		}
		
		if ( ft.length() ) ft.safePrintf(" , ");
		ft.safePrintf("%f",totalSingleScore);

		// min?
		if ( minScore < 0.0 || totalSingleScore < minScore )
			minScore = totalSingleScore;

		// we need to set "ft" for xml stuff below
		if ( si->m_format != FORMAT_HTML ) continue;

		sb->safePrintf("<tr><td><b>%.04f</b></td><td colspan=20>total of above scores</td></tr>", totalSingleScore);
		// close table from printScoresHeader
		if ( ! firstTime ) sb->safePrintf("</table><br>");
	}



	if ( si->m_format == FORMAT_HTML ) {
		sb->safePrintf("<table border=1 cellpadding=3>");
		sb->safePrintf("<tr><td colspan=10><b><center>Ranking bits</center></b></td></tr>");
		sb->safePrintf("<tr><td>Bit</td><td>Score multiplier</td><td>Rank adjustment</td></tr>");

		if(docFlags) {
			for(int bit=0; bit < 26; bit++) {
				if( (1<<bit) & docFlags ) {
					sb->safePrintf("<tr><td>%d</td><td>%f</td><td>%d</td></tr>", bit, si->m_baseScoringParameters.m_flagScoreMultiplier[bit], si->m_baseScoringParameters.m_flagRankAdjustment[bit]);
				}
			}
		}
		else {
			sb->safePrintf("<tr><td colspan=10>None set for document</td></tr>");
		}
		sb->safePrintf("</table><br>");
	}



	const char *ff = "";
	const char *ff2 = "sum";

	// final score!!!
	if ( si->m_format == FORMAT_XML ) {
		sb->safePrintf ("\t\t<siteRank>%" PRId32"</siteRank>\n", (int32_t)dp->m_siteRank );

		sb->safePrintf ("\t\t<numGoodSiteInlinks>%" PRId32 "</numGoodSiteInlinks>\n", (int32_t)mr->m_siteNumInlinks );

		struct tm *timeStruct3;
		time_t pageInlinksLastUpdated = mr->m_pageInlinksLastUpdated;
		struct tm tm_buf;
		timeStruct3 = gmtime_r(&pageInlinksLastUpdated, &tm_buf);
		char tmp3[64];
		strftime(tmp3, 64, "%b-%d-%Y(%H:%M:%S)", timeStruct3);

		// -1 means unknown
		if ( mr->m_pageNumInlinks >= 0 ) {
			// how many inlinks, external and internal, we have
			// to this page not filtered in any way!!!
			sb->safePrintf("\t\t<numTotalPageInlinks>%" PRId32 "</numTotalPageInlinks>\n", mr->m_pageNumInlinks);
		}
		// how many inlinking ips we got, including our own if
		// we link to ourself
		sb->safePrintf("\t\t<numUniqueIpsLinkingToPage>%" PRId32 "</numUniqueIpsLinkingToPage>\n", mr->m_pageNumUniqueIps);
		// how many inlinking cblocks we got, including our own if
		// we link to ourself
		sb->safePrintf("\t\t<numUniqueCBlocksLinkingToPage>%" PRId32 "</numUniqueCBlocksLinkingToPage>\n", mr->m_pageNumUniqueCBlocks);
		
		// how many "good" inlinks. i.e. inlinks whose linktext we
		// count and index.
		sb->safePrintf("\t\t<numGoodPageInlinks>%" PRId32 "</numGoodPageInlinks>\n", mr->m_pageNumGoodInlinks);
		sb->safePrintf("\t\t<pageInlinksLastComputedUTC>%" PRIu32 "</pageInlinksLastComputedUTC>\n", (uint32_t)mr->m_pageInlinksLastUpdated);

		float score = msg40->getScore(ix);
		sb->safePrintf("\t\t<finalScore>%f</finalScore>\n", score);
		sb->safePrintf ("\t\t<finalScoreEquationCanonical>"
			       "<![CDATA["
			       "Final Score = (siteRank*%.01f+1) * "
			       "(%s of above matrix scores)"
			       "]]>"
			       "</finalScoreEquationCanonical>\n"
			       , si->m_baseScoringParameters.m_siteRankMultiplier, ff2);

		sb->safePrintf ("\t\t<finalScoreEquation>"
			       "<![CDATA["
			       "<b>%.03f</b> = (%" PRId32"*%.01f+1) "
			       , dp->m_finalScore, (int32_t)dp->m_siteRank, si->m_baseScoringParameters.m_siteRankMultiplier);

		
		sb->safePrintf(" * %.01f", si->m_baseScoringParameters.m_languageWeights[mr->m_language]);

		// the actual min then
		sb->safePrintf(" * %.03f",minScore);

		// no longer list all the scores
		sb->safePrintf("]]></finalScoreEquation>\n");
		sb->safePrintf ("\t</result>\n\n");
		return true;
	}

	if ( si->m_format != FORMAT_HTML ) return true;

	const char *cc = getCountryCode ( mr->m_country );
	if ( mr->m_country == 0 ) cc = "Unknown";

	float langBoost = si->m_baseScoringParameters.m_languageWeights[mr->m_language];



	sb->safePrintf("<table border=1 cellpadding=3>");
	sb->safePrintf("<tr><td colspan=10><b><center>Final score</center></b></td></tr>");
	sb->safePrintf("<tr><td>DocId</td><td>%" PRId64"</td></tr>", dp->m_docId);
	sb->safePrintf("<tr><td>Site</td><td>%s</td></tr>", mr->ptr_site);
	sb->safePrintf("<tr><td>Language</td><td><font color=green><b>%s</b></font></td></tr>", getLanguageString(mr->m_language)); // use page language
	sb->safePrintf("<tr><td>Language boost</td><td><font color=green><b>%.01f</b></font></td></tr>", langBoost);
	sb->safePrintf("<tr><td>Country</td><td>%s</td></tr>", cc);
	sb->safePrintf("<tr><td>Original SiteRank</td><td><font color=blue>%" PRId32 "</font></td></tr>", (int32_t)dp->m_siteRank);
	sb->safePrintf("<tr><td>Adjusted SiteRank</td><td><font color=blue>%f</font></td></tr>", dp->m_adjustedSiteRank);
	if( dp->m_usePageTemperature ) {
		sb->safePrintf("<tr><td>Page temperature</td><td>%f</td></tr>", dp->m_pageTemperature);
	}
	else {
		sb->safePrintf("<tr><td>Page temperature</td><td>Not used</td></tr>");
	}


	sb->safePrintf("<tr><td colspan=100>");

	// list all final scores starting with pairs
	sb->safePrintf("<b>%f</b> = (<font color=blue>%" PRId32"</font>*%.01f+1)", dp->m_finalScore, (int32_t)dp->m_siteRank, si->m_baseScoringParameters.m_siteRankMultiplier);

	// Check if user specified a query language
	if ( si->m_queryLangId != 0 ) {
		// Query language same as document language?
		if( si->m_queryLangId == mr->m_language ) {
			sb->safePrintf(" * <font color=green><b>%.01f</b></font>", langBoost);
		}
		else
		if( mr->m_language == 0 ) {
			// Document language unknown, use the unknown language weight
			sb->safePrintf(" * <font color=green><b>%.01f</b></font>", langBoost);
		}
	}

	// list all final scores starting with pairs
	sb->safePrintf(" * %s(", ff);
	sb->safeMemcpy ( &ft );
	sb->safePrintf(")</td></tr>");


	sb->safePrintf("</table><br>");
	// put in a hidden div so you can unhide it
	sb->safePrintf("</div>\n");

	// result is in a table so we can put the result # in its own column
	sb->safePrintf("</td></tr></table>");



	// space out 0000 backlinks
	char *p = sb->getBufStart() + placeHolder;
	if ( numInlinks == 0 ) 
		memset(p, ' ', placeHolderLen);
	else if ( numInlinks > 0 && numInlinks < 99999 ) {
		char *ss = strstr ( p, "00000" );
		if ( ss ) {
			char c = ss[5];
			sprintf(ss,"%5" PRId32,numInlinks);
			ss[5] = c;
		}
		// print "1 backlink" not "1 backlinks"
		if ( numInlinks == 1 ) {
			char *xx = strstr(p,"backlinks");
			if ( xx ) xx[8] = ' ';
		}
	}

	return true;
}




static bool printPairScore(SafeBuf *sb, const SearchInput *si, const PairScore *ps, Msg20Reply *mr) {

	// shortcut
	const Query *q = &si->m_q;

	int32_t qtn1 = ps->m_qtermNum1;
	int32_t qtn2 = ps->m_qtermNum2;
	
	unsigned char de1 = ps->m_densityRank1;
	unsigned char de2 = ps->m_densityRank2;
	float dnw1 = getDensityWeight(de1);
	float dnw2 = getDensityWeight(de2);
	
	int32_t hg1 = ps->m_hashGroup1;
	int32_t hg2 = ps->m_hashGroup2;
	
	
	float hgw1 = getHashGroupWeight(hg1);
	float hgw2 = getHashGroupWeight(hg2);
	
	int32_t wp1 = ps->m_wordPos1;
	int32_t wp2 = ps->m_wordPos2;
	
	unsigned char wr1 = ps->m_wordSpamRank1;
	float wsw1 = getWordSpamWeight(wr1);
	unsigned char wr2 = ps->m_wordSpamRank2;
	float wsw2 = getWordSpamWeight(wr2);
	
	// HACK for inlink text!
	if ( hg1 == HASHGROUP_INLINKTEXT )
		wsw1 = getLinkerWeight(wr1);
	if ( hg2 == HASHGROUP_INLINKTEXT )
		wsw2 = getLinkerWeight(wr2);
	
	const char *syn1 = "no";
	const char *syn2 = "no";
	float sw1 = 1.0;
	float sw2 = 1.0;
	if ( ps->m_isSynonym1 ) {
		syn1 = "yes";
		sw1  = g_conf.m_baseScoringParameters.m_synonymWeight;
	}
	if ( ps->m_isSynonym2 ) {
		syn2 = "yes";
		sw2  = g_conf.m_baseScoringParameters.m_synonymWeight;
	}

	const char *bs1  = "no";
	const char *bs2  = "no";
	if ( ps->m_isHalfStopWikiBigram1 ) bs1 = "yes";
	if ( ps->m_isHalfStopWikiBigram2 ) bs2 = "yes";
	float wbw1 = 1.0;
	float wbw2 = 1.0;
	if ( ps->m_isHalfStopWikiBigram1 ) wbw1 = WIKI_BIGRAM_WEIGHT;
	if ( ps->m_isHalfStopWikiBigram2 ) wbw2 = WIKI_BIGRAM_WEIGHT;

	QueryTerm *qt1 = &q->m_qterms[qtn1];
	QueryTerm *qt2 = &q->m_qterms[qtn2];

	int64_t tf1 = qt1->m_termFreq;
	int64_t tf2 = qt2->m_termFreq;
	float tfw1 = ps->m_tfWeight1;
	float tfw2 = ps->m_tfWeight2;
	
	const char *wp = "no";
	float wiw = 1.0;
	if ( ps->m_inSameWikiPhrase ) {
		wp = "yes";
		wiw = WIKI_WEIGHT; // 0.50;
	}
	int32_t a = ps->m_wordPos2;
	int32_t b = ps->m_wordPos1;
	const char *bes = "";
	if ( a < b ) {
		a = ps->m_wordPos1;
		b = ps->m_wordPos2;
		// out of query order penalty!
		bes = "+ <b>1.0</b>";
	}
	
	if ( si->m_format == FORMAT_XML ) {
		sb->safePrintf("\t\t<pairInfo>\n");
		
		sb->safePrintf("\t\t\t<densityRank1>%" PRId32
			      "</densityRank1>\n",
			      (int32_t)de1);
		sb->safePrintf("\t\t\t<densityRank2>%" PRId32
			      "</densityRank2>\n",
			      (int32_t)de2);
		sb->safePrintf("\t\t\t<densityWeight1>%f"
			      "</densityWeight1>\n",
			      dnw1);
		sb->safePrintf("\t\t\t<densityWeight2>%f"
			      "</densityWeight2>\n",
			      dnw2);
		
		sb->safePrintf("\t\t\t<term1><![CDATA[");
		sb->safeMemcpy ( q->m_qterms[qtn1].m_term ,
				q->m_qterms[qtn1].m_termLen );
		sb->safePrintf("]]></term1>\n");
		sb->safePrintf("\t\t\t<term2><![CDATA[");
		sb->safeMemcpy ( q->m_qterms[qtn2].m_term ,
				q->m_qterms[qtn2].m_termLen );
		sb->safePrintf("]]></term2>\n");
		
		sb->safePrintf("\t\t\t<location1><![CDATA[%s]]>"
			      "</location1>\n",
			      getHashGroupString(hg1));
		sb->safePrintf("\t\t\t<location2><![CDATA[%s]]>"
			      "</location2>\n",
			      getHashGroupString(hg2));
		sb->safePrintf("\t\t\t<locationWeight1>%.01f"
			      "</locationWeight1>\n",
			      hgw1 );
		sb->safePrintf("\t\t\t<locationWeight2>%.01f"
			      "</locationWeight2>\n",
			      hgw2 );
		
		sb->safePrintf("\t\t\t<wordPos1>%" PRId32
			      "</wordPos1>\n", wp1 );
		sb->safePrintf("\t\t\t<wordPos2>%" PRId32
			      "</wordPos2>\n", wp2 );

		sb->safePrintf("\t\t\t<isSynonym1>"
			      "<![CDATA[%s]]>"
			      "</isSynonym1>\n",
			      syn1);
		sb->safePrintf("\t\t\t<isSynonym2>"
			      "<![CDATA[%s]]>"
			      "</isSynonym2>\n",
			      syn2);
		sb->safePrintf("\t\t\t<synonymWeight1>%.01f"
			      "</synonymWeight1>\n",
			      sw1);
		sb->safePrintf("\t\t\t<synonymWeight2>%.01f"
			      "</synonymWeight2>\n",
			      sw2);
		
		// word spam / link text weight
		const char *r1 = "wordSpamRank1";
		const char *r2 = "wordSpamRank2";
		const char *t1 = "wordSpamWeight1";
		const char *t2 = "wordSpamWeight2";
		if ( hg1 == HASHGROUP_INLINKTEXT ) {
			r1 = "inlinkSiteRank1";
			t1 = "inlinkTextWeight1";
		}
		if ( hg2 == HASHGROUP_INLINKTEXT ) {
			r2 = "inlinkSiteRank2";
			t2 = "inlinkTextWeight2";
		}
		sb->safePrintf("\t\t\t<%s>%" PRId32"</%s>\n",
			      r1,(int32_t)wr1,r1);
		sb->safePrintf("\t\t\t<%s>%" PRId32"</%s>\n",
			      r2,(int32_t)wr2,r2);
		sb->safePrintf("\t\t\t<%s>%.02f</%s>\n",
			      t1,wsw1,t1);
		sb->safePrintf("\t\t\t<%s>%.02f</%s>\n",
			      t2,wsw2,t2);
		

		// if offsite inlink text show the inlinkid for matching
		// to an <inlink>
		LinkInfo *info = (LinkInfo *)mr->ptr_linkInfo;//inlinks;
		Inlink *k = info ? info->getNextInlink(NULL) : NULL;
		for (;k&&hg1==HASHGROUP_INLINKTEXT ; k=info->getNextInlink(k)){
			if ( ! k->getLinkText() ) continue;
			if ( k->m_wordPosStart > wp1 ) continue;
			if ( k->m_wordPosStart + 50 < wp1 ) continue;
			// got it. we HACKED this to put the id
			// in k->m_siteHash
			sb->safePrintf("\t\t\t<inlinkId1>%" PRId32
				      "</inlinkId1>\n",
				      k->m_siteHash);
		}

		k = info ? info->getNextInlink(NULL) : NULL;
		for (;k&&hg2==HASHGROUP_INLINKTEXT ; k=info->getNextInlink(k)){
			if ( ! k->getLinkText() ) continue;
			if ( k->m_wordPosStart > wp2 ) continue;
			if ( k->m_wordPosStart + 50 < wp2 ) continue;
			// got it. we HACKED this to put the id
			// in k->m_siteHash
			sb->safePrintf("\t\t\t<inlinkId2>%" PRId32
				      "</inlinkId2>\n",
				      k->m_siteHash);
		}

		// term freq
		sb->safePrintf("\t\t\t<termFreq1>%" PRId64
			      "</termFreq1>\n",tf1);
		sb->safePrintf("\t\t\t<termFreq2>%" PRId64
			      "</termFreq2>\n",tf2);
		sb->safePrintf("\t\t\t<termFreqWeight1>%f"
			      "</termFreqWeight1>\n",tfw1);
		sb->safePrintf("\t\t\t<termFreqWeight2>%f"
			      "</termFreqWeight2>\n",tfw2);
		
		sb->safePrintf("\t\t\t<isWikiBigram1>"
			      "%" PRId32"</isWikiBigram1>\n",
			      (int32_t)(ps->m_isHalfStopWikiBigram1));
		sb->safePrintf("\t\t\t<isWikiBigram2>"
			      "%" PRId32"</isWikiBigram2>\n",
			      (int32_t)(ps->m_isHalfStopWikiBigram2));
		
		sb->safePrintf("\t\t\t<wikiBigramWeight1>%.01f"
			      "</wikiBigramWeight1>\n",
			      wbw1);
		sb->safePrintf("\t\t\t<wikiBigramWeight2>%.01f"
			      "</wikiBigramWeight2>\n",
			      wbw2);
		
		sb->safePrintf("\t\t\t<inSameWikiPhrase>"
			      "<![CDATA[%s]]>"
			      "</inSameWikiPhrase>\n",
			      wp);
		
		sb->safePrintf("\t\t\t<queryDist>"
			      "%" PRId32
			      "</queryDist>\n",
			      ps->m_qdist );
		
		sb->safePrintf("\t\t\t<wikiWeight>"
			      "%.01f"
			      "</wikiWeight>\n",
			      wiw );
		
		sb->safePrintf("\t\t\t<score>%f</score>\n",
			      ps->m_finalScore);
		
		sb->safePrintf("\t\t\t<equationCanonical>"
			      "<![CDATA["
			      "score = "
			      " 100 * "
			      " locationWeight1" // hgw
			      " * "
			      " locationWeight2" // hgw
			      " * "
			      " synonymWeight1" // synweight
			      " * "
			      " synonymWeight2" // synweight
			      " * "
			      
			      " wikiBigramWeight1"
			      " * "
			      " wikiBigramWeight2"
			      " * "
			      
			      //"diversityWeight1"
			      //" * "
			      //"diversityWeight2"
			      //" * "
			      "densityWeight1" //density weight
			      " * "
			      "densityWeight2" //density weight
			      " * "
			      "%s" // wordspam weight
			      " * "
			      "%s" // wordspam weight
			      " * "
			      "termFreqWeight1" // tfw
			      " * "
			      "termFreqWeight2" // tfw
			      " / ( ||wordPos1 - wordPos2| "
			      " - queryDist| + 1.0 ) * "
			      "wikiWeight"
			      "]]>"
			      "</equationCanonical>\n"
			      , t1
			      , t2
			      );
		
		sb->safePrintf("\t\t\t<equation>"
			      "<![CDATA["
			      "%f="
			      "100*"
			      "<font color=orange>%.1f</font>"//hashgroupweight
			      "*"
			      "<font color=orange>%.1f</font>"//hashgroupweight
			      "*"
			      "<font color=blue>%.1f</font>" // syn weight
			      "*"
			      "<font color=blue>%.1f</font>" // syn weight
			      "*"
			      
			      "<font color=green>%.1f</font>"//wikibigramweight
			      "*"
			      "<font color=green>%.1f</font>"//wikibigramweight
			      "*"
			      
			      "<font color=purple>%.02f</font>"//density weight
			      "*"
			      "<font color=purple>%.02f</font>"//density weight
			      "*"
			      "<font color=red>%.02f</font>" // wordspam weight
			      "*"
			      "<font color=red>%.02f</font>" // wordspam weight
			      "*"
			      "<font color=magenta>%.02f</font>"//tf weight
			      "*"
			      "<font color=magenta>%.02f</font>"//tf weight
			      , ps->m_finalScore
			      , hgw1
			      , hgw2
			      , sw1
			      , sw2
			      , wbw1
			      , wbw2
			      , dnw1
			      , dnw2
			      , wsw1
			      , wsw2
			      , tfw1
			      , tfw2
			      );
		
		if ( ps->m_fixedDistance )
			sb->safePrintf(
				      "/<b>%" PRId32"</b> "
				      , (int32_t)FIXED_DISTANCE );
		else
			sb->safePrintf(
				      "/"
				      "(((<font color=darkgreen>%" PRId32"</font>"
				      "-<font color=darkgreen>%" PRId32"</font>"
				      ")-<font color=lime>%" PRId32"</font>)+1.0%s)"
				      ,
				      a,b,ps->m_qdist,bes);
		// wikipedia weight
		if ( !almostEqualFloat(wiw, 1.0) )
			sb->safePrintf("*%.01f", wiw );
		sb->safePrintf("]]>"
			      "</equation>\n" );
		sb->safePrintf("\t\t</pairInfo>\n");
		return true; // continue;
	}

	//
	// print first term in first row
	//
	sb->safePrintf("<tr><td rowspan=3>");

	sb->safePrintf("<a onclick=\""
		      "var e = document.getElementById('poo');"
		      "if ( e.style.display == 'none' ){"
		      "e.style.display = '';"
		      "}"
		      "else {"
		      "e.style.display = 'none';"
		      "}"
		      "\">"
		      );
	sb->safePrintf("%.04f</a></td>",ps->m_finalScore);

	sb->safePrintf("<td>"
		      "%s <font color=orange>"
		      "%.01f</font></td>"
		      , getHashGroupString(hg1)
		      , hgw1 );

	// the word position
	sb->safePrintf("<td>");

	sb->safePrintf("<a href=\"/get?d=");

	sb->safePrintf("<a href=\"/get?d=");

	sb->safePrintf("%" PRId64
		       "&page=4"
		       //"&page=sections&"
		       "&hipos=%" PRId32
		       "&c=%s#hipos\">"
		       "%" PRId32"</a></td>"
		       "</a></td>"
		       ,mr->m_docId
		       ,(int32_t)ps->m_wordPos1
		       ,si->m_cr->m_coll
		       ,(int32_t)ps->m_wordPos1);

	sb->safePrintf("<td>%s <font color=blue>%.02f</font></td>",syn1,sw1);

	sb->safePrintf("<td>%s <font color=green>%.02f</font></td>",bs1,wbw1);


	// density
	sb->safePrintf("<td>%" PRId32" <font color=purple>"
		      "%.02f</font></td>",
		      (int32_t)de1,dnw1);
	// word spam
	if ( hg1 == HASHGROUP_INLINKTEXT ) {
		sb->safePrintf("<td>&nbsp;</td>");
		sb->safePrintf("<td>%" PRId32" <font color=red>"
			      "%.02f</font></td>",
			      (int32_t)wr1,wsw1);
	}
	else {
		sb->safePrintf("<td>%" PRId32, (int32_t)wr1);
		//if ( wsw1 != 1.0 )
			sb->safePrintf( " <font color=red>"
				       "%.02f</font>",  wsw1);
		sb->safePrintf("</td>");
		sb->safePrintf("<td>&nbsp;</td>");
	}
	
	// term freq
	sb->safePrintf("<td id=tf>%" PRId64" <font color=magenta>"
		      "%.02f</font></td>",
		      tf1,tfw1);
	// inSamePhraseId distInQuery phraseWeight
	sb->safePrintf("<td>%s</td><td>%" PRId32"</td><td>%.01f</td>"
		       ,wp,ps->m_qdist,wiw);
	// end the row
	sb->safePrintf("</tr>");
	//
	// print 2nd term in 2nd row
	//
	sb->safePrintf("<tr><td>");

	sb->safePrintf(
		      "%s <font color=orange>"
		      "%.01f</font></td>"
		      , getHashGroupString(hg2)
		      , hgw2 );

	// the word position
	sb->safePrintf("<td>");

	sb->safePrintf("<a href=\"/get?d=");

	sb->safePrintf("%" PRId64
		      "&page=4&"
		      "hipos=%" PRId32"&c=%s#hipos\">"
		      "%" PRId32"</a></td>"
		      "</a></td>"
		      ,mr->m_docId
		      ,(int32_t)ps->m_wordPos2
		      ,si->m_cr->m_coll
		      ,(int32_t)ps->m_wordPos2);

	sb->safePrintf("<td>%s <font color=blue>%.02f</font></td>",syn2,sw2);

	sb->safePrintf("<td>%s <font color=green>%.02f</font></td>",bs2,wbw2);
	
	// density
	sb->safePrintf("<td>%" PRId32" <font color=purple>"
		      "%.02f</font></td>",
		      (int32_t)de2,dnw2);
	// word spam
	if ( hg2 == HASHGROUP_INLINKTEXT ) {
		sb->safePrintf("<td>&nbsp;</td>");
		sb->safePrintf("<td>%" PRId32" <font color=red>"
			      "%.02f</font></td>",
			      (int32_t)wr2,wsw2);
	}
	else {
		sb->safePrintf("<td>%" PRId32, (int32_t)wr2);
		//if ( wsw2 != 1.0 )
			sb->safePrintf( " <font color=red>"
				       "%.02f</font>",  wsw2);
		sb->safePrintf("</td>");
		sb->safePrintf("<td>&nbsp;</td>");
	}
	// term freq
	sb->safePrintf("<td id=tf>%" PRId64" <font color=magenta>"
		      "%.02f</font></td>",
		      tf2,tfw2);
	// inSamePhraseId distInQuery phraseWeight
	sb->safePrintf("<td>%s</td><td>%" PRId32"</td><td>%.01f</td>"
		       ,wp,ps->m_qdist,wiw);
	// end the row
	sb->safePrintf("</tr>");
	sb->safePrintf("<tr><td ");

	sb->safePrintf("colspan=50>" //  style=\"display:none\">"
		      "%.03f "
		      "= "
		      //" ( "
		      "100*"
		      "<font color=orange>%.1f"
		      "</font>"
		      "*"
		      "<font color=orange>%.1f"
		      "</font>"
		      "*"
		      //"(%" PRId32" - "
		      , ps->m_finalScore
		      //, idstr
		      , hgw1
		      , hgw2
		      //, (int32_t)MAXWORDPOS+1
		      );
	sb->safePrintf("<font color=blue>%.1f</font>"
		      "*"
		      " <font color=blue>%.1f</font>"
		      "*"
		      
		      // wiki bigram weight
		      "<font color=green>%.02f</font>"
		      "*"
		      "<font color=green>%.02f</font>"
		      "*"
		      
		      "<font color=purple>%.02f</font>"
		      "*"
		      "<font color=purple>%.02f</font>"
		      "*"
		      "<font color=red>%.02f</font>"
		      "*"
		      " <font color=red>%.02f</font>"
		      "*"
		      "<font color=magenta>%.02f</font>"
		      "*"
		      "<font color=magenta>%.02f</font>"
		      , sw1
		      , sw2
		      , wbw1
		      , wbw2
		      , dnw1
		      , dnw2
		      , wsw1
		      , wsw2
		      , tfw1
		      , tfw2
		      );
	if ( ps->m_fixedDistance )
		sb->safePrintf(
			      "/<b>%" PRId32"</b> "
			      , (int32_t)FIXED_DISTANCE );
	else
		sb->safePrintf(
			      "/"
			      "(((<font color=darkgreen>%" PRId32"</font>"
			      "-<font color=darkgreen>%" PRId32"</font>)-"
			      "<font color=lime>%" PRId32"</font>) + 1.0%s)"
			      ,
			      a,b,ps->m_qdist,bes);
	// wikipedia weight
	if ( !almostEqualFloat(wiw, 1.0) )
		sb->safePrintf("*%.01f", wiw );
	sb->safePrintf( // end formula
		      "</td></tr>"
		      //"</table>"
		      //"<br>");
		      );
	return true;
}

static bool printSingleTerm(SafeBuf *sb, const Query *q, const SingleScore *ss) {

	int32_t qtn = ss->m_qtermNum;

	sb->safePrintf("<table border=1 cellpadding=3>");
	sb->safePrintf("<tr><td colspan=50><center><b>");
	// link to rainbow page
	//sb->safePrintf("<a href=\"/print?u=");
	//sb->urlEncode( mr->ptr_ubuf );
	//sb->safePrintf("&page=4&recycle=1&c=%s\">",coll);
	if ( q->m_qterms[qtn].m_isPhrase )
		sb->pushChar('\"');
	sb->safeMemcpy ( q->m_qterms[qtn].m_term ,
			q->m_qterms[qtn].m_termLen );
	if ( q->m_qterms[qtn].m_isPhrase )
		sb->pushChar('\"');
	//sb->safePrintf("</a>");
	sb->safePrintf("</b></center></td></tr>");
	return true;
}

static bool printTermPairs(SafeBuf *sb, const Query *q, const PairScore *ps) {
	// print pair text
	int32_t qtn1 = ps->m_qtermNum1;
	int32_t qtn2 = ps->m_qtermNum2;
	sb->safePrintf("<table cellpadding=3 border=1>"
		      "<tr><td colspan=20><center><b>");
	if ( q->m_qterms[qtn1].m_isPhrase )
		sb->pushChar('\"');
	sb->safeMemcpy ( q->m_qterms[qtn1].m_term ,
			q->m_qterms[qtn1].m_termLen );
	if ( q->m_qterms[qtn1].m_isPhrase )
		sb->pushChar('\"');
	sb->safePrintf("</b> vs <b>");
	if ( q->m_qterms[qtn2].m_isPhrase )
		sb->pushChar('\"');
	sb->safeMemcpy ( q->m_qterms[qtn2].m_term ,
			q->m_qterms[qtn2].m_termLen );
	if ( q->m_qterms[qtn2].m_isPhrase )
		sb->pushChar('\"');
	return true;
}

static bool printScoresHeader ( SafeBuf *sb ) {

	sb->safePrintf("<tr>"
		      "<td>score</td>"
		      "<td>location</td>"
		      "<td>wordPos</td>"
		      "<td>synonym</td>"
		      "<td>wikibigram</td>"
		      //"<td>diversityRank</td>"
		      "<td>density</td>"
		      "<td>spam</td>"
		      "<td>inlinkPR</td>" // nlinkSiteRank</td>"
		      "<td>termFreq</td>"
		       "<td>inSamePhrase</td>"
		       "<td>distInQuery</td>"
		       "<td>phraseWeight</td>"
		      "</tr>\n" 
		      );
	return true;
}

static bool printSingleScore(SafeBuf *sb, const SearchInput *si, const SingleScore *ss, Msg20Reply *mr) {

	// shortcut
	const Query *q = &si->m_q;

	//SafeBuf ft;
	// store in final score calc
	//if ( ft.length() ) ft.safePrintf(" + ");
	//ft.safePrintf("%f",ss->m_finalScore);
	const char *syn = "no";
	float sw = 1.0;
	if ( ss->m_isSynonym ) {
		syn = "yes";
		sw = g_conf.m_baseScoringParameters.m_synonymWeight;
	}
	//char bf = ss->m_bflags;
	float wbw = 1.0;
	const char *bs = "no";
	if ( ss->m_isHalfStopWikiBigram ) {
		bs = "yes";
		wbw = WIKI_BIGRAM_WEIGHT;
	}
	float hgw = getHashGroupWeight(ss->m_hashGroup);
	float dnw = getDensityWeight(ss->m_densityRank);
	float wsw = getWordSpamWeight(ss->m_wordSpamRank);
	// HACK for inlink text!
	if ( ss->m_hashGroup == HASHGROUP_INLINKTEXT )
		wsw = getLinkerWeight(ss->m_wordSpamRank);
	
	//int64_t tf = ss->m_termFreq;//ss->m_listSize;
	int32_t qtn = ss->m_qtermNum;
	//int64_t tf = msg40->m_msg3a.m_termFreqs[qtn];
	QueryTerm *qt = &q->m_qterms[qtn];
	int64_t tf = qt->m_termFreq;
	float tfw = ss->m_tfWeight;
	
	if ( si->m_format == FORMAT_JSON ) {
		sb->safePrintf("\t\t\"terminfo\": {\n");

		sb->safePrintf("\t\t\t\"densityRank\": %" PRId32",\n", (int32_t)ss->m_densityRank);
		sb->safePrintf("\t\t\t\"densityWeight\": %f,\n", dnw);
		sb->safePrintf("\t\t\t\"term\": \"%*.*s\",\n", (int)q->m_qterms[qtn].m_termLen, (int)q->m_qterms[qtn].m_termLen,q->m_qterms[qtn].m_term);
		
		sb->safePrintf("\t\t\t\"location\": \"%s\",\n", getHashGroupString(ss->m_hashGroup));
		sb->safePrintf("\t\t\t\"locationWeight\": %.01f,\n", hgw );
		sb->safePrintf("\t\t\t\"wordPos\": %" PRId32",\n", (int32_t)ss->m_wordPos );
		sb->safePrintf("\t\t\t\"isSynonym\": \"%s\",\n", syn);
		sb->safePrintf("\t\t\t\"synonymWeight\": %.01f,\n", sw);
		sb->safePrintf("\t\t\t\"isWikiBigram\": %" PRId32",\n", (int32_t)(ss->m_isHalfStopWikiBigram) );
		sb->safePrintf("\t\t\t\"wikiBigramWeight\": %.01f,\n", (float)WIKI_BIGRAM_WEIGHT);
		// word spam
		if ( ss->m_hashGroup == HASHGROUP_INLINKTEXT ) {
			sb->safePrintf("\t\t\t\"inlinkSiteRank>%" PRId32",\n", (int32_t)ss->m_wordSpamRank);
			sb->safePrintf("\t\t\t\"inlinkTextWeight\": %.02f,\n", wsw);
		}
		else {
			sb->safePrintf("\t\t\t\"wordSpamRank\": %" PRId32",\n", (int32_t)ss->m_wordSpamRank);
			sb->safePrintf("\t\t\t\"wordSpamWeight\": %.02f,\n", wsw);
		}


		// if offsite inlink text show the inlinkid for matching
		// to an <inlink>
		LinkInfo *info = (LinkInfo *)mr->ptr_linkInfo;//inlinks;
		Inlink *k = info ? info->getNextInlink(NULL) : NULL;
		sb->safePrintf("\t\t\t\"inlinkIds\": [,\n");
		for ( ; k && ss->m_hashGroup==HASHGROUP_INLINKTEXT ;
		      k=info->getNextInlink(k)){
			if ( ! k->getLinkText() ) continue;
			if ( k->m_wordPosStart > ss->m_wordPos ) continue;
			if ( k->m_wordPosStart + 50 < ss->m_wordPos ) continue;
			// got it. we HACKED this to put the id
			// in k->m_siteHash
			sb->safePrintf("\t\t\t\t\"%" PRId32",\n", k->m_siteHash);
		}
		sb->safePrintf("\t\t\t],\n");

		// term freq
		sb->safePrintf("\t\t\t\"termFreq\": %" PRId64",\n", tf);
		sb->safePrintf("\t\t\t\"termFreqWeight\": %f,\n", tfw);
		
		sb->safePrintf("\t\t\t\"score\": %f,\n", ss->m_finalScore);
		
		sb->safePrintf("\t\t\t\"equationCanonical\": \""
			      "score = "
			      " 100 * "
			      " locationWeight" // hgw
			      " * "
			      " locationWeight" // hgw
			      " * "
			      " synonymWeight" // synweight
			      " * "
			      " synonymWeight" // synweight
			      " * "
			      " wikiBigramWeight"
			      " * "
			      " wikiBigramWeight"
			      " * "
			      //" diversityWeight" // divweight
			      //" * "
			      //" diversityWeight" // divweight
			      //" * "
			      "densityWeight" // density weight
			      " * "
			      "densityWeight" // density weight
			      " * "
			      "wordSpamWeight" // wordspam weight
			      " * "
			      "wordSpamWeight" // wordspam weight
			      " * "
			      "termFreqWeight" // tfw
			      " * "
			      "termFreqWeight" // tfw
			      "\",\n"
			      );
		
		sb->safePrintf("\t\t\t\"equation>\": \""
			      "%f="
			      "100*"
			      "%.1f" // hgw
			      "*"
			      "%.1f" // hgw
			      "*"
			      "%.1f" // synweight
			      "*"
			      "%.1f" // synweight
			      "*"
			      "%.02f" // wikibigram weight
			      "*"
			      "%.02f" // wikibigram weight
			      "*"
			      "%.02f" // density weight
			      "*"
			      "%.02f" // density weight
			      "*"
			      "%.02f" // wordspam weight
			      "*"
			      "%.02f" // wordspam weight
			      "*"
			      "%.02f" // tfw
			      "*"
			      "%.02f" // tfw
			      "\"\n"
			      , ss->m_finalScore
			      , hgw
			      , hgw
			      , sw
			      , sw
			      , wbw
			      , wbw
			      , dnw
			      , dnw
			      , wsw
			      , wsw
			      , tfw
			      , tfw
			      );
		sb->safePrintf("\t\t}");
		return true;
	}

	if ( si->m_format == FORMAT_XML ) {
		sb->safePrintf("\t\t<termInfo>\n");

		sb->safePrintf("\t\t\t<densityRank>%" PRId32
			      "</densityRank>\n",
			      (int32_t)ss->m_densityRank);
		sb->safePrintf("\t\t\t<densityWeight>%f"
			      "</densityWeight>\n",
			      dnw);
		sb->safePrintf("\t\t\t<term><![CDATA[");
		sb->safeMemcpy ( q->m_qterms[qtn].m_term ,
				q->m_qterms[qtn].m_termLen );
		sb->safePrintf("]]></term>\n");
		
		sb->safePrintf("\t\t\t<location><![CDATA[%s]]>"
			      "</location>\n",
			      getHashGroupString(ss->m_hashGroup));
		sb->safePrintf("\t\t\t<locationWeight>%.01f"
			      "</locationWeight>\n",
			      hgw );
		sb->safePrintf("\t\t\t<wordPos>%" PRId32
			      "</wordPos>\n", (int32_t)ss->m_wordPos );
		sb->safePrintf("\t\t\t<isSynonym>"
			      "<![CDATA[%s]]>"
			      "</isSynonym>\n",
			      syn);
		sb->safePrintf("\t\t\t<synonymWeight>%.01f"
			      "</synonymWeight>\n",
			      sw);
		sb->safePrintf("\t\t\t<isWikiBigram>%" PRId32
			      "</isWikiBigram>\n",
			      (int32_t)(ss->m_isHalfStopWikiBigram) );
		sb->safePrintf("\t\t\t<wikiBigramWeight>%.01f"
			      "</wikiBigramWeight>\n",
			      (float)WIKI_BIGRAM_WEIGHT);
		// word spam
		if ( ss->m_hashGroup == HASHGROUP_INLINKTEXT ) {
			sb->safePrintf("\t\t\t<inlinkSiteRank>%" PRId32
				      "</inlinkSiteRank>\n",
				      (int32_t)ss->m_wordSpamRank);
			sb->safePrintf("\t\t\t<inlinkTextWeight>%.02f"
				      "</inlinkTextWeight>\n",
				      wsw);
		}
		else {
			sb->safePrintf("\t\t\t<wordSpamRank>%" PRId32
				      "</wordSpamRank>\n",
				      (int32_t)ss->m_wordSpamRank);
			sb->safePrintf("\t\t\t<wordSpamWeight>%.02f"
				      "</wordSpamWeight>\n",
				      wsw);
		}


		// if offsite inlink text show the inlinkid for matching
		// to an <inlink>
		LinkInfo *info = (LinkInfo *)mr->ptr_linkInfo;//inlinks;
		Inlink *k = info ? info->getNextInlink(NULL) : NULL;
		for ( ; k && ss->m_hashGroup==HASHGROUP_INLINKTEXT ; 
		      k=info->getNextInlink(k)){
			if ( ! k->getLinkText() ) continue;
			if ( k->m_wordPosStart > ss->m_wordPos ) continue;
			if ( k->m_wordPosStart + 50 < ss->m_wordPos ) continue;
			// got it. we HACKED this to put the id
			// in k->m_siteHash
			sb->safePrintf("\t\t\t<inlinkId>%" PRId32
				      "</inlinkId>\n",
				      k->m_siteHash);
		}

		// term freq
		sb->safePrintf("\t\t\t<termFreq>%" PRId64
			      "</termFreq>\n",tf);
		sb->safePrintf("\t\t\t<termFreqWeight>%f"
			      "</termFreqWeight>\n",tfw);
		
		sb->safePrintf("\t\t\t<score>%f</score>\n",
			      ss->m_finalScore);
		
		sb->safePrintf("\t\t\t<equationCanonical>"
			      "<![CDATA["
			      "score = "
			      " 100 * "
			      " locationWeight" // hgw
			      " * "
			      " locationWeight" // hgw
			      " * "
			      " synonymWeight" // synweight
			      " * "
			      " synonymWeight" // synweight
			      " * "
			      
			      " wikiBigramWeight"
			      " * "
			      " wikiBigramWeight"
			      " * "
			      
			      //" diversityWeight" // divweight
			      //" * "
			      //" diversityWeight" // divweight
			      //" * "
			      "densityWeight" // density weight
			      " * "
			      "densityWeight" // density weight
			      " * "
			      "wordSpamWeight" // wordspam weight
			      " * "
			      "wordSpamWeight" // wordspam weight
			      " * "
			      "termFreqWeight" // tfw
			      " * "
			      "termFreqWeight" // tfw
			      //" / ( 3.0 )"
			      "]]>"
			      "</equationCanonical>\n"
			      );
		
		sb->safePrintf("\t\t\t<equation>"
			      "<![CDATA["
			      "%f="
			      "100*"
			      "%.1f" // hgw
			      "*"
			      "%.1f" // hgw
			      "*"
			      
			      "%.1f" // synweight
			      "*"
			      "%.1f" // synweight
			      "*"
			      
			      
			      "%.02f" // wikibigram weight
			      "*"
			      "%.02f" // wikibigram weight
			      "*"
			      
			      "%.02f" // density weight
			      "*"
			      "%.02f" // density weight
			      "*"
			      "%.02f" // wordspam weight
			      "*"
			      "%.02f" // wordspam weight
			      "*"
			      "%.02f" // tfw
			      "*"
			      "%.02f" // tfw
			      //" / ( 3.0 )"
			      "]]>"
			      "</equation>\n"
			      , ss->m_finalScore
			      , hgw
			      , hgw
			      , sw
			      , sw
			      , wbw
			      , wbw
			      , dnw
			      , dnw
			      , wsw
			      , wsw
			      , tfw
			      , tfw
			      );
		sb->safePrintf("\t\t</termInfo>\n");
		return true;
	}



	sb->safePrintf("<tr>"
		      "<td rowspan=2>%.03f</td>\n"
		      "<td>%s <font color=orange>%.1f"
		      "</font></td\n>"
		      // wordpos
		      "<td>"
		      "<a href=\"/get?d=" 
		      , ss->m_finalScore
		      , getHashGroupString(ss->m_hashGroup)
		      , hgw
		      );
	//sb->urlEncode( mr->ptr_ubuf );
	sb->safePrintf("%" PRId64,mr->m_docId );
	sb->safePrintf("&page=4&"
		      "hipos=%" PRId32"&c=%s#hipos\">"
		      ,(int32_t)ss->m_wordPos
		      ,si->m_cr->m_coll);
	sb->safePrintf("%" PRId32"</a></td>\n"
		      "<td>%s <font color=blue>%.1f"
		      "</font></td>\n" // syn
		      
		      // wikibigram?/weight
		      "<td>%s <font color=green>%.02f</font></td>\n"
		      
		      //"<td>%" PRId32"/<font color=green>%f"
		      //"</font></td>" // diversity
		      "<td>%" PRId32" <font color=purple>"
		      "%.02f</font></td>\n" // density
		      , (int32_t)ss->m_wordPos
		      , syn
		      , sw // synonym weight
		      , bs
		      , wbw
		      //, (int32_t)ss->m_diversityRank
		      //, dvw
		      , (int32_t)ss->m_densityRank
		      , dnw
		      );
	if ( ss->m_hashGroup == HASHGROUP_INLINKTEXT ) {
		sb->safePrintf("<td>&nbsp;</td>"
			      "<td>%" PRId32" <font color=red>%.02f"
			      "</font></td>\n" // wordspam
			      , (int32_t)ss->m_wordSpamRank
			      , wsw
			      );
	}
	else {
		sb->safePrintf("<td>%" PRId32" <font color=red>%.02f"
			      "</font></td>" // wordspam
			      "<td>&nbsp;</td>\n"
			      , (int32_t)ss->m_wordSpamRank
			      , wsw
			      );
		
	}
	
	sb->safePrintf("<td id=tf>%" PRId64" <font color=magenta>"
		      "%.02f</font></td>\n" // termfreq
		      "</tr>\n"
		      , tf
		      , tfw
		      );
	// last row is the computation of score
	sb->safePrintf("<tr><td colspan=50>"
		      "%.03f "
		      " = "
		      //" %" PRId32" * "
		      "100 * "
		      " <font color=orange>%.1f</font>"
		      " * "
		      " <font color=orange>%.1f</font>"
		      " * "
		      " <font color=blue>%.1f</font>"
		      " * "
		      " <font color=blue>%.1f</font>"
		      " * "
		      " <font color=green>%.02f</font>"//wikibigramwght
		      " * "
		      " <font color=green>%.02f</font>"
		      " * "
		      "<font color=purple>%.02f</font>"
		      " * "
		      "<font color=purple>%.02f</font>"
		      " * "
		      "<font color=red>%.02f</font>"
		      " * "
		      "<font color=red>%.02f</font>"
		      " * "
		      "<font color=magenta>%.02f</font>"
		      " * "
		      "<font color=magenta>%.02f</font>"
		      //" / ( 3.0 )"
		      // end formula
		      "</td></tr>\n"
		      , ss->m_finalScore
		      //, (int32_t)MAXWORDPOS+1
		      , hgw
		      , hgw
		      , sw
		      , sw
		      , wbw
		      , wbw
		      //, dvw
		      //, dvw
		      , dnw
		      , dnw
		      , wsw
		      , wsw
		      , tfw
		      , tfw
		      );
	//sb->safePrintf("</table>"
	//	      "<br>");
	return true;
}

// if catId >= 1 then print the dmoz radio button
static bool printLogoAndSearchBox(SafeBuf *sb, HttpRequest *hr, const SearchInput *si) {
	const char *coll = hr->getString("c");
	if ( ! coll ) coll = "";

	// if there's a ton of sites use the post method otherwise
	// they won't fit into the http request, the browser will reject
	// sending such a large request with "GET"
	const char *method = "GET";
	if ( si && si->m_sites && strlen(si->m_sites)>800 ) {
		method = "POST";
	}

	sb->safePrintf(
		      //
		      // search box
		      //
		      "<form name=f method=%s action=/search>\n\n" 

		      // propagate the collection if they re-search
		      "<input name=c type=hidden value=\"%s\">"
		       , method
		      , coll
		      );

	// propagate prepend
	const char *prepend = hr->getString("prepend");
	if ( prepend ) {
		sb->safePrintf("<input name=prepend type=hidden value=\"");
		sb->htmlEncode ( prepend, strlen(prepend), false);
		sb->safePrintf("\">");
	}
	

	// put search box in a box
	sb->safePrintf(
		       "<br>"
		       "<br>"
		       "<br>"
		       "<div style="
		       "background-color:#fcc714;"
		       "border-style:solid;"
		       "border-width:3px;"
		       "border-color:blue;"
		       //"background-color:blue;"
		       "padding:20px;"
		       "border-radius:20px;"
		       ">");


	sb->safePrintf (
			//"<div style=margin-left:5px;margin-right:5px;>
			"<input size=40 type=text name=q "

			"style=\""
			//"width:%" PRId32"px;"
			"height:26px;"
			"padding:0px;"
			"font-weight:bold;"
			"padding-left:5px;"
			//"border-radius:10px;"
			"margin:0px;"
			"border:1px inset lightgray;"
			"background-color:#ffffff;"
			"font-size:18px;"
			"\" "


			"value=\""
			);

	// contents of search box
	int32_t  qlen;
	const char *qstr = hr->getString("q",&qlen,"",NULL);
	sb->htmlEncode ( qstr , qlen , false );

	// if it was an advanced search, this can be empty
	if ( qlen == 0 && si && si->m_displayQuery )
		sb->htmlEncode ( si->m_displayQuery );

	sb->safePrintf ("\">"
			"&nbsp; &nbsp;"

			"<div onclick=document.f.submit(); "

			" onmouseover=\""
			"this.style.backgroundColor='lightgreen';"
			"this.style.color='black';\""
			" onmouseout=\""
			"this.style.backgroundColor='green';"
			"this.style.color='white';\" "

			"style=border-radius:28px;"
			"cursor:pointer;"
			"cursor:hand;"
			"border-color:white;"
			"border-style:solid;"
			"border-width:3px;"
			"padding:12px;"
			"width:20px;"
			"height:20px;"
			"display:inline-block;"
			"background-color:green;color:white;>"
			"<b style=margin-left:-5px;font-size:18px;"
			">GO</b>"
			"</div>"
			);

	sb->safePrintf(	"</div>"
			"<br>"
			"<br>"
		       );


	printSearchFiltersBar ( sb , hr );
	

	sb->safePrintf( "</form>\n" );
	return true;
}



class MenuItem {
public:
	int32_t  m_menuNum;
	const char *m_title;
	// we append this to the url
	const char *m_cgi;
	char  m_tmp[25];
};


const static int MAX_MENU_ENTRIES = 200;

static MenuItem s_mi[MAX_MENU_ENTRIES];
static int32_t s_num = 0;

static int getNextMenuEntryIndex(int curr_index)
{
	int i = curr_index+1;
	if( i >= MAX_MENU_ENTRIES )
	{
		g_process.shutdownAbort(true);
	}
	return i;
}

static bool printSearchFiltersBar ( SafeBuf *sb , HttpRequest *hr ) {
	SafeBuf cu;
	hr->getCurrentUrl ( cu );


	sb->safePrintf("<script>"
		       "function show(id){"
		       "var e = document.getElementById(id);"
		       "if ( e.style.display == 'none' ){"
		       "e.style.display = '';"
		       "}"
		       "else {"
		       "e.style.display = 'none';"
		       "}"
		       "}"
		       "</script>"
		       );


	static bool s_init = false;

	if ( ! s_init ) {

		int32_t menuNum = 0;
		int32_t n = 0;

		s_mi[n].m_menuNum  = menuNum;
		s_mi[n].m_title    = "Any time";
		s_mi[n].m_cgi      = "secsback=0";
		n = getNextMenuEntryIndex(n);

		s_mi[n].m_menuNum  = menuNum;
		s_mi[n].m_title    = "Past 24 hours";
		s_mi[n].m_cgi      = "secsback=86400";
		n = getNextMenuEntryIndex(n);

		s_mi[n].m_menuNum  = menuNum;
		s_mi[n].m_title    = "Past week";
		s_mi[n].m_cgi      = "secsback=604800";
		n = getNextMenuEntryIndex(n);

		s_mi[n].m_menuNum  = menuNum;
		s_mi[n].m_title    = "Past month";
		s_mi[n].m_cgi      = "secsback=2592000";
		n = getNextMenuEntryIndex(n);

		s_mi[n].m_menuNum  = menuNum;
		s_mi[n].m_title    = "Past year";
		s_mi[n].m_cgi      = "secsback=31536000";
		n = getNextMenuEntryIndex(n);

		++menuNum;

		// sort by
		s_mi[n].m_menuNum  = menuNum;
		s_mi[n].m_title    = "Sorted by relevance";
		s_mi[n].m_cgi      = "sortby=0";
		n = getNextMenuEntryIndex(n);

		s_mi[n].m_menuNum  = menuNum;
		s_mi[n].m_title    = "Sorted by date";
		s_mi[n].m_cgi      = "sortby=1";
		n = getNextMenuEntryIndex(n);

		s_mi[n].m_menuNum  = menuNum;
		s_mi[n].m_title    = "Reverse sorted by date";
		s_mi[n].m_cgi      = "sortby=2";
		n = getNextMenuEntryIndex(n);

		++menuNum;

		// languages
		s_mi[n].m_menuNum  = menuNum;
		s_mi[n].m_title    = "Any language";
		s_mi[n].m_cgi      = "qlang=xx";
		n = getNextMenuEntryIndex(n);

		for ( int32_t i = 0 ; i < langLast ; i++ ) {
			s_mi[n].m_menuNum  = menuNum;
			s_mi[n].m_title    = getLanguageString(i);
			const char *abbr = getLanguageAbbr(i);
			snprintf(s_mi[n].m_tmp,10,"qlang=%s",abbr);
			s_mi[n].m_cgi      = s_mi[n].m_tmp;
			n = getNextMenuEntryIndex(n);
		}

		++menuNum;

		// filetypes
		s_mi[n].m_menuNum  = menuNum;
		s_mi[n].m_title    = "Any filetype";
		s_mi[n].m_cgi      = "filetype=any";
		n = getNextMenuEntryIndex(n);

		s_mi[n].m_menuNum  = menuNum;
		s_mi[n].m_title    = "HTML";
		s_mi[n].m_cgi      = "filetype=html";
		n = getNextMenuEntryIndex(n);

		s_mi[n].m_menuNum  = menuNum;
		s_mi[n].m_title    = "TEXT";
		s_mi[n].m_cgi      = "filetype=txt";
		n = getNextMenuEntryIndex(n);

		s_mi[n].m_menuNum  = menuNum;
		s_mi[n].m_title    = "PDF";
		s_mi[n].m_cgi      = "filetype=pdf";
		n = getNextMenuEntryIndex(n);

		s_mi[n].m_menuNum  = menuNum;
		s_mi[n].m_title    = "Microsoft Word";
		s_mi[n].m_cgi      = "filetype=doc";
		n = getNextMenuEntryIndex(n);

		s_mi[n].m_menuNum  = menuNum;
		s_mi[n].m_title    = "XML";
		s_mi[n].m_cgi      = "filetype=xml";
		n = getNextMenuEntryIndex(n);

		s_mi[n].m_menuNum  = menuNum;
		s_mi[n].m_title    = "JSON";
		s_mi[n].m_cgi      = "filetype=json";
		n = getNextMenuEntryIndex(n);

		s_mi[n].m_menuNum  = menuNum;
		s_mi[n].m_title    = "Excel";
		s_mi[n].m_cgi      = "filetype=xls";
		n = getNextMenuEntryIndex(n);

		s_mi[n].m_menuNum  = menuNum;
		s_mi[n].m_title    = "PostScript";
		s_mi[n].m_cgi      = "filetype=ps";
		n = getNextMenuEntryIndex(n);

		s_mi[n].m_menuNum  = menuNum;
		s_mi[n].m_title    = "Spider Status";
		s_mi[n].m_cgi      = "filetype=status";
		n = getNextMenuEntryIndex(n);

		++menuNum;

		// output
		s_mi[n].m_menuNum  = menuNum;
		s_mi[n].m_title    = "Output HTML";
		s_mi[n].m_cgi      = "format=html";
		n = getNextMenuEntryIndex(n);

		s_mi[n].m_menuNum  = menuNum;
		s_mi[n].m_title    = "Output XML";
		s_mi[n].m_cgi      = "format=xml";
		n = getNextMenuEntryIndex(n);

		s_mi[n].m_menuNum  = menuNum;
		s_mi[n].m_title    = "Output JSON";
		s_mi[n].m_cgi      = "format=json";
		n = getNextMenuEntryIndex(n);

		s_mi[n].m_menuNum  = menuNum;
		s_mi[n].m_title    = "Output CSV";
		s_mi[n].m_cgi      = "format=csv";
		n = getNextMenuEntryIndex(n);

		++menuNum;

		// show/hide banned
		s_mi[n].m_menuNum  = menuNum;
		s_mi[n].m_title    = "Hide banned results";
		s_mi[n].m_cgi      = "sb=0";
		n = getNextMenuEntryIndex(n);

		s_mi[n].m_menuNum  = menuNum;
		s_mi[n].m_title    = "Show banned results";
		s_mi[n].m_cgi      = "sb=1";
		n = getNextMenuEntryIndex(n);

		++menuNum;

		// spider status
		s_mi[n].m_menuNum  = menuNum;
		s_mi[n].m_title    = "Hide Spider Log";
		s_mi[n].m_cgi      = "splog=0";
		n = getNextMenuEntryIndex(n);

		s_mi[n].m_menuNum  = menuNum;
		s_mi[n].m_title    = "Show Spider Log";
		s_mi[n].m_cgi      = "q=type:status";
		n = getNextMenuEntryIndex(n);

		++menuNum;

		// family filter
		s_mi[n].m_menuNum  = menuNum;
		s_mi[n].m_title    = "Family Filter Off";
		s_mi[n].m_cgi      = "ff=0";
		n = getNextMenuEntryIndex(n);

		s_mi[n].m_menuNum  = menuNum;
		s_mi[n].m_title    = "Family Filter On";
		s_mi[n].m_cgi      = "ff=1";
		n = getNextMenuEntryIndex(n);

		++menuNum;

		// META TAGS
		s_mi[n].m_menuNum  = menuNum;
		s_mi[n].m_title    = "No Meta Tags";
		s_mi[n].m_cgi      = "dt=";
		n = getNextMenuEntryIndex(n);

		s_mi[n].m_menuNum  = menuNum;
		s_mi[n].m_title    = "Show Meta Tags";
		s_mi[n].m_cgi      = "dt=keywords+description";
		n = getNextMenuEntryIndex(n);

		++menuNum;

		// ADMIN
		s_mi[n].m_menuNum  = menuNum;
		s_mi[n].m_title    = "Show Admin View";
		s_mi[n].m_cgi      = "admin=1";
		n = getNextMenuEntryIndex(n);

		s_mi[n].m_menuNum  = menuNum;
		s_mi[n].m_title    = "Show User View";
		s_mi[n].m_cgi      = "admin=0";
		n = getNextMenuEntryIndex(n);

		++menuNum;

		// fx_country
		s_mi[n].m_menuNum  = menuNum;
		s_mi[n].m_title    = "fx_country (none)";
		s_mi[n].m_cgi      = "fx_country=";
		n = getNextMenuEntryIndex(n);

		s_mi[n].m_menuNum  = menuNum;
		s_mi[n].m_title    = "de";
		s_mi[n].m_cgi      = "fx_country=de";
		n = getNextMenuEntryIndex(n);

		s_mi[n].m_menuNum  = menuNum;
		s_mi[n].m_title    = "dk";
		s_mi[n].m_cgi      = "fx_country=dk";
		n = getNextMenuEntryIndex(n);

		s_mi[n].m_menuNum  = menuNum;
		s_mi[n].m_title    = "fr";
		s_mi[n].m_cgi      = "fx_country=fr";
		n = getNextMenuEntryIndex(n);

		s_mi[n].m_menuNum  = menuNum;
		s_mi[n].m_title    = "no";
		s_mi[n].m_cgi      = "fx_country=no";
		n = getNextMenuEntryIndex(n);

		s_mi[n].m_menuNum  = menuNum;
		s_mi[n].m_title    = "se";
		s_mi[n].m_cgi      = "fx_country=se";
		n = getNextMenuEntryIndex(n);

		++menuNum;

		// fx_blang
		s_mi[n].m_menuNum  = menuNum;
		s_mi[n].m_title    = "fx_blang (none)";
		s_mi[n].m_cgi      = "fx_blang=";
		n = getNextMenuEntryIndex(n);

		s_mi[n].m_menuNum  = menuNum;
		s_mi[n].m_title    = "da";
		s_mi[n].m_cgi      = "fx_blang=da";
		n = getNextMenuEntryIndex(n);

		s_mi[n].m_menuNum  = menuNum;
		s_mi[n].m_title    = "de";
		s_mi[n].m_cgi      = "fx_blang=de";
		n = getNextMenuEntryIndex(n);

		s_mi[n].m_menuNum  = menuNum;
		s_mi[n].m_title    = "en";
		s_mi[n].m_cgi      = "fx_blang=en";
		n = getNextMenuEntryIndex(n);

		s_mi[n].m_menuNum  = menuNum;
		s_mi[n].m_title    = "en-US";
		s_mi[n].m_cgi      = "fx_blang=en-US";
		n = getNextMenuEntryIndex(n);

		s_mi[n].m_menuNum  = menuNum;
		s_mi[n].m_title    = "no";
		s_mi[n].m_cgi      = "fx_blang=no";
		n = getNextMenuEntryIndex(n);

		s_mi[n].m_menuNum  = menuNum;
		s_mi[n].m_title    = "se";
		s_mi[n].m_cgi      = "fx_blang=se";
		n = getNextMenuEntryIndex(n);

		++menuNum;

		// fx_fetld
		s_mi[n].m_menuNum  = menuNum;
		s_mi[n].m_title    = "fx_fetld (none)";
		s_mi[n].m_cgi      = "fx_fetld=";
		n = getNextMenuEntryIndex(n);

		s_mi[n].m_menuNum  = menuNum;
		s_mi[n].m_title    = "com";
		s_mi[n].m_cgi      = "fx_fetld=com";
		n = getNextMenuEntryIndex(n);

		s_mi[n].m_menuNum  = menuNum;
		s_mi[n].m_title    = "de";
		s_mi[n].m_cgi      = "fx_fetld=de";
		n = getNextMenuEntryIndex(n);

		s_mi[n].m_menuNum  = menuNum;
		s_mi[n].m_title    = "dk";
		s_mi[n].m_cgi      = "fx_fetld=dk";
		n = getNextMenuEntryIndex(n);

		s_mi[n].m_menuNum  = menuNum;
		s_mi[n].m_title    = "no";
		s_mi[n].m_cgi      = "fx_fetld=no";
		n = getNextMenuEntryIndex(n);

		s_mi[n].m_menuNum  = menuNum;
		s_mi[n].m_title    = "se";
		s_mi[n].m_cgi      = "fx_fetld=se";
		n = getNextMenuEntryIndex(n);

		s_num = n;
		s_init = true;
	}


	// we'll print the admin menu custom since it's mostly off-page links

	// bar of drop down menus
	sb->safePrintf("<div style=color:gray;>");

	for ( int32_t i = 0 ; i <= s_mi[s_num-1].m_menuNum ; i++ ) {
		// after 5 make a new line
		if ( i % 5 == 0 ) sb->safePrintf("<br><br>");

		printMenu ( sb , i , hr );
	}

	sb->safePrintf("</div>\n");
	sb->safePrintf("<br>\n");

	return true;
}

static bool printMenu ( SafeBuf *sb , int32_t menuNum , HttpRequest *hr ) {

	bool firstOne = true;

	MenuItem *first = NULL;

	const char *src    = hr->getOrigUrlRequest();
	int32_t  srcLen = hr->getOrigUrlRequestLen();

	const char *frontTag = "";
	const char *backTag = "";

	bool isDefaultHeader = true;

	// try to set first based on what's in the url
	for ( int32_t i = 0 ; i < s_num ; i++ ) {
		// shortcut
		MenuItem *mi = &s_mi[i];
		// skip if not our item
		if ( mi->m_menuNum != menuNum ) continue;

		// is it in the url
		const char *match = strnstr ( src, mi->m_cgi, srcLen );

		// or if empty quotes it is the true header like
		// for 'hide spider log' option
		if ( ! match ) {
			isDefaultHeader = false;
			continue;
		}
		// ensure ? or & preceeds
		if ( match > src && match[-1] != '?' && match[-1] != '&' )
			continue;
		// and \0 or & follows
		int32_t milen = strlen(mi->m_cgi);
		if ( match+milen > src+srcLen ) continue;
		if ( ! is_wspace_a(match[milen]) && match[milen] != '&' ) 
			continue;
		// got it
		first = mi;
		// do not highlight the orig header
		if ( isDefaultHeader ) break;
		frontTag = "<b style=color:maroon;>";
		backTag = "</b>";
		break;
	}
	

	for ( int32_t i = 0 ; i < s_num ; i++ ) {

		// shortcut
		MenuItem *mi = &s_mi[i];

		// skip if not our item
		if ( mi->m_menuNum != menuNum ) continue;

		if ( ! first ) first = mi;

		if ( ! firstOne ) goto skip;

		firstOne = false;

		// for centering the dropdown
		sb->safePrintf("<span style=position:relative;></span>");

		// print hidden drop down menu
		sb->safePrintf(
			       "<span id=menu%" PRId32" style=\"display:none;"
			       "position:absolute;"
			       //"margin-left:-20px;"
			       "margin-top:15px;"
			       "width:150px;"
			       "max-height:300px;"
			       "overflow-y:auto;"
			       "background-color:white;"
			       "padding:10px;"
			       "width=80px;border-width:1px;"
			       "border-color:lightgray;"
			       "box-shadow: -.5px 1px 1px gray;"
			       "border-style:solid;color:gray;\" "

			       //" onmouseout=\""
			       //"this.style.display='none';\""

			       // if clicking on scrollbar do not hide menu!
			       " onmousedown=\"inmenuclick=1;\" "

			       ">"
			       , mi->m_menuNum
			       );

	skip:

		
		// . add our cgi to the original url
		// . so if it has &qlang=de and they select &qlang=en
		//   we have to replace it... etc.
		StackBuf<512> newUrl;
		replaceParm ( mi->m_cgi , &newUrl , hr );
		newUrl += '\0';

		// print each item in there
		sb->safePrintf("<a href=%s>"
			       "<div style=cursor:pointer;cursor:hand;"
			       "padding-top:10px;"
			       "padding-bottom:10px;"
			       "color:gray;"

			       " onmouseover=\""
			       "this.style.backgroundColor='#e0e0e0';\" "
			       " onmouseout=\""
			       "this.style.backgroundColor='white';\" "

			       // prevent the body onmousedown from 
			       // hiding the menu
			       " onmousedown=\"inmenuclick=1;\" "

			       ">"
			       "<nobr>"
			       , newUrl.getBufStart()
			       );

		// print checkmark (check mark) next to selected one
		// if not the default (trueHeader)
		if ( mi == first ) // ! isDefaultHeader && mi == first )
			sb->safePrintf("<b style=color:black;>&check;</b>");
		else 
			sb->safePrintf("&nbsp; &nbsp; ");

		sb->safePrintf(" %s</nobr>"
			       "</div>"
			       "</a>"
			       , mi->m_title );

		//sb->safePrintf("<br><br>");
	}

	// wrap up the drop down
	sb->safePrintf("</span>");
	


	// print heading or current selection i guess
	sb->safePrintf(
		       // separate menus with these two spaces
		       " &nbsp; &nbsp; "
		       // print the menu header that when clicked
		       // will show the drop down
		       "<span style=cursor:pointer;"
		       "cursor:hand; "

		       "onmousedown=\"this.style.color='red';"
		       "inmenuclick=1;"
		       "\" "

		       "onmouseup=\"this.style.color='gray';"

		       // close any other open menu
		       "if ( openmenu !='') {"
		       "document.getElementById(openmenu)."
		       "style.display='none'; "
		       "var saved=openmenu;"
		       "openmenu='';"
		       // don't reopen our same menu below!
		       "if ( saved=='menu%" PRId32"') return;"
		       "}"

		       // show our menu
		       "show('menu%" PRId32"'); "
		       // we are now open
		       "openmenu='menu%" PRId32"'; "

		       "\""
		       ">"

		       "%s%s%s %c%c%c" 
		       "</span>"
		       , first->m_menuNum
		       , first->m_menuNum
		       , first->m_menuNum
		       , frontTag
		       , first->m_title
		       , backTag
		       // print triangle
		       ,0xe2
		       ,0x96
		       ,0xbc
		       );


	return true;
}

static bool replaceParm ( const char *cgi , SafeBuf *newUrl , HttpRequest *hr ) { 
	if ( ! cgi[0] ) return true;
	// get original request url. this is not \0 terminated
	const char *src    = hr->getOrigUrlRequest();
	int32_t  srcLen = hr->getOrigUrlRequestLen();
	return replaceParm2 ( cgi ,newUrl, src, srcLen );
}

static bool replaceParm2 ( const char *cgi , SafeBuf *newUrl , 
			   const char *oldUrl , int32_t oldUrlLen ) {

	const char *src    = oldUrl;
	int32_t  srcLen = oldUrlLen;

	const char *srcEnd = src + srcLen;

	const char *equal = strstr(cgi,"=");
	if ( ! equal ) {
		log(LOG_WARN, "results: %s has no equal sign", cgi);
		return false;
	}
	int32_t cgiLen = equal - cgi;

	const char *p = src;

 tryagain:

	const char *found = strncasestr ( p , cgi , srcEnd - p , cgiLen );

	// if no ? or & before it it is bogus!
	if ( found && found[-1] != '&' && found[-1] != '?' ) {
		// try again
		p = found + 1;
		goto tryagain;
	}
		
	// fix &s= replaceing &sb=
	if ( found && found[cgiLen] != '=' ) {
		// try again
		p = found + 1;
		goto tryagain;
	}


	// if no collision, just append it
	if ( ! found ) {
		if ( ! newUrl->safeMemcpy ( src , srcLen ) ) return false;
		if ( ! newUrl->pushChar('&') ) return false;
		if ( ! newUrl->safeStrcpy ( cgi ) ) return false;
		if ( ! newUrl->nullTerm() ) return false;
		return true;
	}

	// . otherwise we have to replace it
	// . copy up to where it starts
	if ( ! newUrl->safeMemcpy ( src , found-src ) ) return false;
	// then insert our new cgi there
	if ( ! newUrl->safeStrcpy ( cgi ) ) return false;
	// then resume it
	const char *foundEnd = strncasestr ( found , "&" , srcEnd - found );
	// if nothing came after...
	if ( ! foundEnd ) {
		if ( ! newUrl->nullTerm() ) return false;
		return true;
	}
	// copy over what came after
	if ( ! newUrl->safeMemcpy ( foundEnd, srcEnd-foundEnd ) ) return false;
	if ( ! newUrl->nullTerm() ) return false;
	return true;
}

static bool printMetaContent ( Msg40 *msg40 , int32_t i , State0 *st, SafeBuf *sb ) {
	// store the user-requested meta tags content
	SearchInput *si = &st->m_si;
	const char *pp      =      si->m_displayMetas;
	const char *ppend   = pp + strlen(si->m_displayMetas);
	Msg20 *m = msg40->m_msg20[i];//getMsg20(i);
	Msg20Reply *mr = m->m_r;
	char *dbuf    = mr->ptr_dbuf;//msg40->getDisplayBuf(i);
	int32_t  dbufLen = mr->size_dbuf-1;//msg40->getDisplayBufLen(i);
	char *dbufEnd = dbuf + (dbufLen-1);
	char *dptr    = dbuf;
	//bool  printedSomething = false;
	// loop over the names of the requested meta tags
	while ( pp < ppend && dptr < dbufEnd ) {
		// . assure last byte of dbuf is \0
		//   provided dbufLen > 0
		// . this insures sprintf and strlen won't
		//   crash on dbuf/dptr
		if ( dbuf [ dbufLen ] != '\0' ) {
			log(LOG_LOGIC,"query: Meta tag buffer has no \\0.");
			break;
		}
		// skip initial spaces
		while ( pp < ppend && is_wspace_a(*pp) ) pp++;
		// break if done
		if ( ! *pp ) break;
		// that's the start of the meta tag name
		const char *ss = pp;
		// . find end of that meta tag name
		// . can end in :<integer> -- specifies max len
		while ( pp < ppend && ! is_wspace_a(*pp) && 
			*pp != ':' ) pp++;
		size_t sslen = (size_t)(pp-ss);
		// if ':' was specified, skip the rest
		if ( *pp == ':' ) while ( pp < ppend && ! is_wspace_a(*pp)) pp++;
		// print the name
		int32_t ddlen = dbufLen;
		// newspaperarchive wants tags printed even if no value
		// make sure the meta tag isn't fucked up
		for ( int32_t ti = 0; ti < ddlen; ti++ ) {
			if ( dptr[ti] == '"' ||
			     dptr[ti] == '>' ||
			     dptr[ti] == '<' ||
			     dptr[ti] == '\r' ||
			     dptr[ti] == '\n' ||
			     dptr[ti] == '\0' ) {
				ddlen = ti;
				break;
			}
		}

		if ( ddlen > 0 ) {
			// ship it out
			if ( si->m_format == FORMAT_XML ) {
				sb->safePrintf ( "\t\t<display name=\"%.*s\">"
					         "<![CDATA[", (int)sslen, ss);
				cdataEncode(sb, dptr, ddlen);
				sb->safePrintf ( "]]></display>\n" );
			}
			else if ( si->m_format == FORMAT_JSON ) {
				sb->safePrintf ( "\t\t\"display.%.*s\":\"", (int)sslen, ss);
				sb->jsonEncode ( dptr, ddlen );
				sb->safePrintf ( "\",\n");
			}
			// otherwise, print in light gray
			else {
				sb->safePrintf("<font color=#c62939>"
					      "<b>%.*s</b>: ", (int)sslen, ss);
				sb->safeMemcpy ( dptr, ddlen );
				sb->safePrintf ( "</font><br>" );
			}
		}
		// point to next content of tag to display
		dptr += ddlen + 1;
	}
	return true;
}