diff --git a/app.py b/app.py
index 0a55bbb..83a2a0e 100644
--- a/app.py
+++ b/app.py
@@ -113,6 +113,10 @@ for group in theological_synonym_groups:
# Map each word to other words in its group
theological_synonyms_map[word] = [w for w in group if w != word]
+# Load concept mappings for common theological terms not in KJV
+concept_mappings = json.load(open("data/concept_mappings.json", "r"))
+concept_mappings_dict = {mapping["query"].lower(): mapping for mapping in concept_mappings}
+
cached: dict = {
"verse_ids": [i[0] for i in kjv_cur.execute("SELECT id FROM fts_kjv;").fetchall()],
"book_names": [
@@ -123,6 +127,7 @@ cached: dict = {
"json_dictionary": json.load(open("data/1828_Webster_KJV.json", "r")),
"grammatical_variants": grammatical_variants_map,
"theological_synonyms": theological_synonyms_map,
+ "concept_mappings": concept_mappings_dict,
"html_response": open("html/response.html", "r").read(),
"html_navbar": open("html/navbar.html", "r").read(),
"css_navbar": open("styles/header.css", "r").read(),
@@ -470,34 +475,55 @@ def parse_query(query_override: str = None):
return send_file("html/find.html")
if query.endswith(","):
query = query[:-1]
- result = show(kw=query, human_readable=human_readable)
- if not hasattr(result, "__len__"):
- logging.debug(f"parse_query.show = '{result}'")
- return result
- if len(result) > 17:
- return result
- result = search()
+ # Check if query contains both letters and digits (required for reference parsing)
+ # If only numbers or only letters, skip reference parsing and go to keyword search
+ has_letters = any(char.isalpha() for char in query)
+ has_digits = any(char.isdigit() for char in query)
+
+ if not (has_letters and has_digits):
+ result = search()
+ else:
+ result = show(kw=query, human_readable=human_readable)
+ if not hasattr(result, "__len__"):
+ logging.debug(f"parse_query.show = '{result}'")
+ return result
+
+ if len(result) > 17:
+ return result
+ # Reference parsing failed, fall back to keyword search
+ result = search()
# Remove the closing from search() so we can add dictionary content inside .responses
if result.endswith(""):
result = result[:-6] # Remove last
- result += ""
- result += "
1828 Webster Definitions
Click on the word to get the definition.
"
+
+ # Check if we have any dictionary definitions before showing the section
split_query = query.replace(",", " ").split(" ")
if len(split_query) > 20:
return "Your Query is too long. 20 Words or less."
+
+ definitions_html = ""
+ has_definitions = False
for word in split_query:
definition = define(word)
if not definition:
continue
- result += (
+ has_definitions = True
+ definitions_html += (
""
)
- result += "
" + definition + "
"
+ definitions_html += "
" + definition + "
"
logging.debug(f"parse_query.define = '{word}'")
- result += ""
+
+ # Only add the dictionary section if we found at least one definition
+ if has_definitions:
+ result += ""
+ result += "
1828 Webster Definitions
Click on the word to get the definition.
"
+ result += definitions_html
+ result += ""
+
result += ""
return result
@@ -663,7 +689,7 @@ def response_rich(scripture):
def scripture_response(scriptures, human_readable=False):
response = []
- arg_view = request.args.get("view", "json")
+ arg_view = request.args.get("view", "rich")
logging.debug(f"search ->requested_format = {arg_view}")
if arg_view == "plain":
for scripture in scriptures:
@@ -789,7 +815,88 @@ def search():
return send_file("html/search.html")
else:
return send_file("html/search.html")
+
+ # Check if this is a common theological concept not in KJV
+ concept_mapping = cached["concept_mappings"].get(cleaned_keywords.lower(), None)
+ concept_notice = None
+ if concept_mapping:
+ # Build hyperlinks for all search terms
+ original_query = cleaned_keywords
+ search_links = []
+ for term in concept_mapping["search_terms"]:
+ search_link = f"/search?kw={term.replace(' ', '+')}&view=rich"
+ search_links.append(f"{term}")
+
+ links_html = ", ".join(search_links)
+ concept_notice = f"
{original_query.capitalize()}: {concept_mapping['explanation']} You may find what you are looking for with these search terms: {links_html}
"
+
+ # Use the first search term for the actual search
+ cleaned_keywords = concept_mapping["search_terms"][0]
+
+ def score_result_by_proximity(verse_text, query_words):
+ """
+ Score a verse based on how close together the query words appear.
+ Higher score = words appear closer together.
+ """
+ verse_lower = verse_text.lower()
+
+ # Find all positions of each query word
+ word_positions = {}
+ for word in query_words:
+ word_lower = word.lower()
+ positions = []
+ start = 0
+ while True:
+ pos = verse_lower.find(word_lower, start)
+ if pos == -1:
+ break
+ positions.append(pos)
+ start = pos + 1
+ word_positions[word] = positions
+
+ # If any word is missing, score is 0
+ if any(len(positions) == 0 for positions in word_positions.values()):
+ return 0
+
+ # Calculate minimum distance between all query words
+ # For each combination of positions, find the span (max_pos - min_pos)
+ # Smaller span = higher score
+ import itertools
+
+ # Get all combinations of positions for all words
+ all_position_combos = list(itertools.product(*word_positions.values()))
+
+ if not all_position_combos:
+ return 0
+
+ # Find the combination with minimum span
+ min_span = float('inf')
+ for combo in all_position_combos:
+ span = max(combo) - min(combo)
+ if span < min_span:
+ min_span = span
+
+ # Convert span to score (smaller span = higher score)
+ # Use inverse: 1000 / (span + 1) so adjacent words score ~1000, far apart score low
+ score = 1000.0 / (min_span + 1)
+
+ return score
+
results = lookup_fts(cleaned_keywords)
+
+ # Sort results by proximity score if we have multiple query words
+ if results and len(cleaned_keywords.split()) > 1:
+ query_words = cleaned_keywords.split()
+ scored_results = []
+ for result in results:
+ verse_text = result[4] # The text is at index 4
+ score = score_result_by_proximity(verse_text, query_words)
+ scored_results.append((score, result))
+
+ # Sort by score descending, then keep just the results
+ scored_results.sort(key=lambda x: x[0], reverse=True)
+ results = [result for score, result in scored_results]
+
if not results:
# PRIORITY 1: Try grammatical variant expansion first
words = cleaned_keywords.split()
@@ -904,9 +1011,45 @@ def search():
# PRIORITY 4: If we still have no results, show spelling suggestions
suggestions = h.suggest(cleaned_keywords)
- return "{} not found, try one of the following: {}".format(
- cleaned_keywords, ", ".join(suggestions) if suggestions else "No suggestions available"
- )
+ arg_view = request.args.get("view", "rich")
+
+ if arg_view == "rich":
+ # Build a nice no-results page with header
+ no_results_html = cached["html_navbar"].format(
+ query=cleaned_keywords,
+ current_chapter_name="",
+ next_chapter_name="",
+ next_chapter_link="",
+ prev_chapter_name="",
+ prev_chapter_link=""
+ )
+ no_results_html += cached["css_navbar"]
+ no_results_html += "
"
+ no_results_html += f"
No results found for '{cleaned_keywords}'.
"
+
+ if suggestions:
+ no_results_html += "
Did you mean?
"
+ no_results_html += "
We couldn't find any verses matching your search. Here are some spelling suggestions:
"
+ for suggestion in suggestions[:5]: # Limit to 5 suggestions
+ search_link = f"/search?kw={suggestion}&view=rich"
+ no_results_html += f"
"
+ return no_results_html
+ else:
+ # Plain text fallback
+ return "{} not found, try one of the following: {}".format(
+ cleaned_keywords, ", ".join(suggestions) if suggestions else "No suggestions available"
+ )
response_list = scripture_response(results)
# Check if any words in the search have theological synonyms to suggest
@@ -918,15 +1061,27 @@ def search():
for synonym in synonyms:
theological_suggestions.append(synonym)
- arg_view = request.args.get("view", None)
+ arg_view = request.args.get("view", "rich")
if arg_view == "plain":
response = " ".join(response_list)
+ if concept_notice:
+ response = concept_notice + " " + response
if theological_suggestions:
response += "
Also consider searching: "
for suggestion in theological_suggestions:
response += f"{suggestion} "
elif arg_view == "rich":
response = "".join(response_list)
+ # Insert concept notice at the top if it exists
+ if concept_notice:
+ # Find the first after the opening
and insert after it
+ responses_start = response.find("
")
+ if responses_start != -1:
+ # Find the results-count div closing
+ insert_pos = response.find("
", responses_start + len("
"))
+ if insert_pos != -1:
+ insert_pos += len("
")
+ response = response[:insert_pos] + concept_notice + response[insert_pos:]
if theological_suggestions:
# Add suggestions before the LAST closing
which closes the responses container
suggestions_html = "
Related Theological Concepts
"
diff --git a/data/concept_mappings.json b/data/concept_mappings.json
new file mode 100644
index 0000000..8001fa3
--- /dev/null
+++ b/data/concept_mappings.json
@@ -0,0 +1,142 @@
+[
+ {
+ "query": "rapture",
+ "explanation": "The word 'rapture' does not appear in the KJV, but is usually a reference to believers being caught up to meet Christ in the air.",
+ "search_terms": ["caught up", "meet the lord in the air"]
+ },
+ {
+ "query": "trinity",
+ "explanation": "The word 'trinity' does not appear in the KJV, but is usually a reference to the doctrine of the triune God (Father, Son, and Holy Spirit).",
+ "search_terms": ["father son holy ghost", "three that bear record"]
+ },
+ {
+ "query": "missionary",
+ "explanation": "The word 'missionary' does not appear in the KJV, but is usually a reference to being sent to preach the gospel.",
+ "search_terms": ["go ye therefore", "preach the gospel", "sent"]
+ },
+ {
+ "query": "bible",
+ "explanation": "The word 'bible' does not appear in the KJV, but is usually a reference to the scriptures and God's word.",
+ "search_terms": ["scripture", "word of god", "holy scriptures"]
+ },
+ {
+ "query": "inerrancy",
+ "explanation": "The word 'inerrancy' does not appear in the KJV, but is usually a reference to the perfection and preservation of God's word.",
+ "search_terms": ["word of god", "scripture", "thou hast magnified thy word"]
+ },
+ {
+ "query": "antichrist system",
+ "explanation": "The phrase 'antichrist system' is usually a reference to the antichrist and end times deception.",
+ "search_terms": ["antichrist", "man of sin", "son of perdition", "beast"]
+ },
+ {
+ "query": "pre-tribulation",
+ "explanation": "The phrase 'pre-tribulation' does not appear in the KJV, but is usually a reference to Christ's return before the tribulation.",
+ "search_terms": ["caught up", "great tribulation", "day of the lord"]
+ },
+ {
+ "query": "soul winning",
+ "explanation": "The phrase 'soul winning' does not appear in the KJV, but is usually a reference to winning souls and converting sinners.",
+ "search_terms": ["winneth souls", "save souls", "convert"]
+ },
+ {
+ "query": "guardian angel",
+ "explanation": "The phrase 'guardian angel' does not appear in the KJV, but is usually a reference to angels and their protective ministry.",
+ "search_terms": ["angel", "ministering spirits", "charge over thee"]
+ },
+ {
+ "query": "angel wings",
+ "explanation": "The phrase 'angel wings' does not appear in the KJV, but is usually a reference to biblical descriptions of angels.",
+ "search_terms": ["angel", "seraphim", "cherubim", "wings"]
+ },
+ {
+ "query": "7 deadly sins",
+ "explanation": "The phrase '7 deadly sins' does not appear in the KJV (it's a Catholic tradition), but is usually a reference to biblical lists of sins.",
+ "search_terms": ["abomination", "works of the flesh", "evil"]
+ },
+ {
+ "query": "purgatory",
+ "explanation": "The word 'purgatory' does not appear in the KJV. This is a Catholic doctrine not found in scripture.",
+ "search_terms": ["appointed unto men once to die", "after this the judgment", "absent from the body"]
+ },
+ {
+ "query": "christmas",
+ "explanation": "The word 'christmas' does not appear in the KJV, but is usually a reference to the birth of Christ.",
+ "search_terms": ["born in bethlehem", "manger", "wise men", "shepherds"]
+ },
+ {
+ "query": "easter",
+ "explanation": "The word 'easter' appears once in the KJV (Acts 12:4) referring to Passover, but is usually a reference to Christ's resurrection.",
+ "search_terms": ["easter", "resurrection", "rose again", "third day"]
+ },
+ {
+ "query": "lucifer",
+ "explanation": "The name 'Lucifer' appears once in the KJV (Isaiah 14:12), but is usually a reference to Satan and his fall.",
+ "search_terms": ["lucifer", "devil", "satan", "dragon", "serpent"]
+ },
+ {
+ "query": "666",
+ "explanation": "The number '666' is usually a reference to the mark of the beast.",
+ "search_terms": ["six hundred threescore and six", "mark", "number of his name"]
+ },
+ {
+ "query": "four horsemen",
+ "explanation": "The phrase 'four horsemen' does not appear exactly in the KJV, but is usually a reference to the four horsemen of the apocalypse.",
+ "search_terms": ["horse", "seal", "rider"]
+ },
+ {
+ "query": "end of the world",
+ "explanation": "The phrase 'end of the world' is usually a reference to the end times and Christ's return.",
+ "search_terms": ["end of the world", "last days", "coming of the lord"]
+ },
+ {
+ "query": "left behind",
+ "explanation": "The phrase 'left behind' is usually a reference to the rapture and second coming.",
+ "search_terms": ["one shall be taken", "left", "coming of the son of man"]
+ },
+ {
+ "query": "prosperity gospel",
+ "explanation": "The phrase 'prosperity gospel' does not appear in the KJV. This is a false doctrine. Biblical warnings against covetousness and false teachers:",
+ "search_terms": ["love of money", "covetousness", "false prophets", "perverse disputings"]
+ },
+ {
+ "query": "speaking in tongues",
+ "explanation": "The phrase 'speaking in tongues' is usually a reference to tongues and spiritual gifts.",
+ "search_terms": ["tongues", "unknown tongue", "interpretation"]
+ },
+ {
+ "query": "slain in the spirit",
+ "explanation": "The phrase 'slain in the spirit' does not appear in the KJV. This is a charismatic practice not found in scripture.",
+ "search_terms": ["holy ghost", "filled with the spirit", "spirit of god"]
+ },
+ {
+ "query": "original sin",
+ "explanation": "The phrase 'original sin' does not appear in the KJV, but is usually a reference to the doctrine of inherited sin from Adam.",
+ "search_terms": ["all have sinned", "by one man sin entered", "in adam all die"]
+ },
+ {
+ "query": "the chosen",
+ "explanation": "The phrase 'the chosen' is usually a reference to God's elect and chosen people.",
+ "search_terms": ["chosen", "elect", "called"]
+ },
+ {
+ "query": "good samaritan",
+ "explanation": "The phrase 'good samaritan' is usually a reference to the parable of the good Samaritan.",
+ "search_terms": ["certain samaritan", "neighbour", "priest levite"]
+ },
+ {
+ "query": "prodigal son",
+ "explanation": "The phrase 'prodigal son' is usually a reference to the parable of the prodigal son.",
+ "search_terms": ["younger son", "riotous living", "father said", "was lost"]
+ },
+ {
+ "query": "jezebel spirit",
+ "explanation": "The phrase 'jezebel spirit' does not appear in the KJV (it's a charismatic term), but is usually a reference to Jezebel and false prophetesses.",
+ "search_terms": ["jezebel", "that woman", "seduceth"]
+ },
+ {
+ "query": "generational curse",
+ "explanation": "The phrase 'generational curse' does not appear in the KJV (it's a charismatic term), but is usually a reference to visiting iniquity upon children.",
+ "search_terms": ["visiting the iniquity", "fathers", "children"]
+ }
+]
diff --git a/data/grammatical_variants.json b/data/grammatical_variants.json
index f171d69..069b0c6 100644
--- a/data/grammatical_variants.json
+++ b/data/grammatical_variants.json
@@ -25,5 +25,30 @@
["love", "loves", "loveth", "lovest"],
["believe", "believes", "believeth", "believest"],
["did", "didst"],
- ["eat", "eats", "eateth", "eatest"]
+ ["eat", "eats", "eateth", "eatest"],
+ ["among", "amongst"],
+ ["until", "till", "unto"],
+ ["afterward", "afterwards"],
+ ["before", "ere"],
+ ["also", "moreover", "furthermore"],
+ ["therefore", "wherefore", "thus"],
+ ["because", "for"],
+ ["indeed", "verily"],
+ ["behold", "lo"],
+ ["ask", "asks", "asketh", "askest"],
+ ["find", "finds", "findeth", "findest"],
+ ["seek", "seeks", "seeketh", "seekest"],
+ ["trust", "trusts", "trusteth", "trustest"],
+ ["follow", "follows", "followeth", "followest"],
+ ["serve", "serves", "serveth", "servest"],
+ ["teach", "teaches", "teacheth", "teachest"],
+ ["think", "thinks", "thinketh", "thinkest"],
+ ["lead", "leads", "leadeth", "leadest"],
+ ["rejoice", "rejoices", "rejoiceth", "rejoicest"],
+ ["keep", "keeps", "keepeth", "keepest"],
+ ["abide", "abides", "abideth", "abidest"],
+ ["judge", "judges", "judgeth", "judgest"],
+ ["die", "dies", "dieth", "diest"],
+ ["rise", "rises", "riseth", "risest"],
+ ["reign", "reigns", "reigneth", "reignest"]
]
diff --git a/data/t_kjv.db b/data/t_kjv.db
new file mode 100644
index 0000000..e69de29
diff --git a/data/theological_synonyms.json b/data/theological_synonyms.json
index bc077a0..9721899 100644
--- a/data/theological_synonyms.json
+++ b/data/theological_synonyms.json
@@ -3,5 +3,102 @@
["ghost", "spirit"],
["righteousness", "holiness"],
["salvation", "redemption"],
- ["faith", "belief"]
+ ["faith", "belief"],
+ ["sin", "iniquity", "transgression"],
+ ["forgive", "pardon"],
+ ["anger", "wrath"],
+ ["witness", "testify", "testimony"],
+ ["heaven", "paradise"],
+ ["hell", "damnation"],
+ ["pray", "prayer", "supplication"],
+ ["bless", "blessed", "blessing"],
+ ["promise", "covenant"],
+ ["worship", "praise"],
+ ["disciple", "apostle"],
+ ["eternal", "everlasting"],
+ ["demon", "devil"],
+ ["miracle", "wonder", "sign"],
+ ["prophesy", "prophecy"],
+ ["resurrect", "resurrection", "rise", "risen"],
+ ["baptize", "baptism"],
+ ["tempt", "temptation"],
+ ["humble", "meek"],
+ ["wise", "wisdom"],
+ ["truth", "true"],
+ ["glory", "glorify"],
+ ["mercy", "compassion"],
+ ["grace", "favor"],
+ ["judge", "judgment", "condemn", "condemnation"],
+ ["command", "commandment"],
+ ["rejoice", "joy", "gladness"],
+ ["reverence", "fear"],
+ ["teach", "doctrine"],
+ ["preach", "proclaim"],
+ ["suffer", "suffering", "affliction"],
+ ["comfort", "console"],
+ ["obey", "obedience"],
+ ["peace", "rest"],
+ ["repent", "repentance", "turn"],
+ ["holy", "sanctify", "sanctified"],
+ ["nation", "gentile"],
+ ["Israel", "Jew", "Hebrew"],
+ ["king", "kingdom", "reign"],
+ ["priest", "priesthood"],
+ ["covenant", "testament"],
+ ["temple", "tabernacle", "sanctuary"],
+ ["altar", "sacrifice", "offering"],
+ ["righteous", "just", "justice"],
+ ["wicked", "evil", "ungodly"],
+ ["servant", "minister"],
+ ["fornication", "whoredom", "adultery"],
+ ["sodomite", "abomination"],
+ ["pervert", "corrupt"],
+ ["destroy", "destruction", "perish"],
+ ["harlot", "whore"],
+ ["idolater", "idolatry"],
+ ["covetous", "covetousness", "greed"],
+ ["enemy", "adversary"],
+ ["avenge", "vengeance", "recompense"],
+ ["tribulation", "trouble", "distress"],
+ ["scorn", "mock", "despise"],
+ ["proud", "pride", "haughty"],
+ ["meek", "lowly", "humble"],
+ ["wisdom", "understanding", "knowledge"],
+ ["foolish", "fool", "folly"],
+ ["strengthen", "strong", "might"],
+ ["weary", "faint"],
+ ["prevail", "overcome"],
+ ["remnant", "residue"],
+ ["inheritance", "heir"],
+ ["generation", "seed", "offspring"],
+ ["pestilence", "plague"],
+ ["famine", "hunger"],
+ ["captivity", "bondage"],
+ ["deliver", "deliverance", "save"],
+ ["redeem", "ransom"],
+ ["provoke", "anger", "wrath"],
+ ["abhor", "hate", "detest"],
+ ["trust", "confidence"],
+ ["persecute", "persecution"],
+ ["blaspheme", "blasphemy"],
+ ["stone", "stoning"],
+ ["burn", "fire", "flame"],
+ ["beloved", "wellbeloved"],
+ ["delight", "pleasure"],
+ ["kindness", "lovingkindness"],
+ ["tender", "tenderness"],
+ ["longsuffering", "patience", "forbearance"],
+ ["gentleness", "meekness"],
+ ["goodness", "kindness"],
+ ["brotherly", "brother"],
+ ["wrath", "indignation", "fury"],
+ ["vengeance", "recompense", "reward"],
+ ["end times", "last days", "latter days"],
+ ["appearing", "coming", "return"],
+ ["trumpet", "shout", "voice"],
+ ["watch", "watchful", "vigilant"],
+ ["seal", "sealed"],
+ ["vial", "bowl"],
+ ["scroll", "book"],
+ ["lamb", "lion"]
]
diff --git a/styles/header.css b/styles/header.css
index 5cee892..57566e5 100644
--- a/styles/header.css
+++ b/styles/header.css
@@ -287,8 +287,8 @@ button:active {
background-color: var(--color-bg-secondary);
border-left: 3px solid var(--color-accent-primary);
border-radius: var(--border-radius-sm);
- padding: 0.75rem 1.25rem;
- margin: 0.5rem 0;
+ padding: 0.5rem 1rem;
+ margin: 0.15rem 0;
transition: background-color var(--transition-fast), border-color var(--transition-fast);
line-height: 1;
}
@@ -324,10 +324,10 @@ button:active {
.verse-text {
font-family: var(--font-scripture);
font-size: 1.15rem;
- line-height: 1.8;
+ line-height: 1.6;
color: var(--color-text-primary);
text-indent: 0;
- margin: 0.35rem 0 0 0;
+ margin: 0.25rem 0 0 0;
padding: 0;
display: block;
}