Repository: 8400TheHealthNetwork/HebSafeHarbor (language: python)

Issue:
Docker updates + Documentations
- updated docker-compose.yml
- added docker-compose-development.yml
- updated README.md
- fix crashing docker setup issue due to missing README.md


Discussion / hints:

Fix patch:
diff --git a/.github/workflows/python-app.yml b/.github/workflows/python-app.yml
new file mode 100644
index 0000000..be5178a
--- /dev/null
+++ b/.github/workflows/python-app.yml
@@ -0,0 +1,36 @@
+# This workflow will install Python dependencies, run tests and lint with a single version of Python
+# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions
+
+name: Python application
+
+on:
+  push:
+    branches: [ main ]
+  pull_request:
+    branches: [ main ]
+
+jobs:
+  build:
+
+    runs-on: ubuntu-latest
+
+    steps:
+    - uses: actions/checkout@v2
+    - name: Set up Python 3.8
+      uses: actions/setup-python@v2
+      with:
+        python-version: "3.8"
+    - name: Install dependencies
+      run: |
+        python -m pip install --upgrade pip
+        pip install flake8 pytest
+        if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
+    - name: Lint with flake8
+      run: |
+        # stop the build if there are Python syntax errors or undefined names
+        flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
+        # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
+        flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
+    - name: Test with pytest
+      run: |
+        pytest
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..bda4623
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,31 @@
+# using two stage build to save space, based on https://pythonspeed.com/articles/multi-stage-docker-python/
+
+FROM python:3.9-slim AS compile-image
+
+WORKDIR /usr/src/app
+RUN apt-get update
+RUN apt-get install -y --no-install-recommends build-essential gcc
+RUN python3 -m venv /opt/venv
+# Make sure we use the virtualenv:
+ENV PATH="/opt/venv/bin:$PATH"
+
+COPY requirements.txt ./
+COPY requirements_server.txt ./
+COPY setup.py ./
+COPY hebsafeharbor ./hebsafeharbor
+RUN python3 -m pip install --upgrade pip
+RUN python3 -m pip install -r requirements.txt --no-cache-dir
+RUN python3 -m pip install -r requirements_server.txt --no-cache-dir
+RUN python3 setup.py install
+
+
+FROM python:3.9-slim AS build-image
+WORKDIR /usr/src/app
+COPY hsh_service.py ./
+COPY server.py ./
+COPY --from=compile-image /opt/venv /opt/venv
+# Make sure we use the virtualenv:
+ENV PATH="/opt/venv/bin:$PATH"
+#run server and expose port 8000 for listening
+EXPOSE 8080
+ENTRYPOINT ["python","server.py"]
diff --git a/README.md b/README.md
index f3f5dbe..20080e3 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,52 @@
 # HebSafeHarbor
-Hebrew PHI identification and redaction toolkit
+
+A de-identification toolkit for clinical text in Hebrew.
+
+[![MIT license](https://img.shields.io/badge/license-MIT-brightgreen.svg)](http://opensource.org/licenses/MIT) ![Release](https://img.shields.io/github/v/release/8400TheHealthNetwork/HebSafeHarbor.svg) [![PyPI version](https://badge.fury.io/py/hebsafeharbor.svg)](https://badge.fury.io/py/hebsafeharbor) [![Pypi Downloads](https://img.shields.io/pypi/dm/hebsafeharbor.svg)](https://img.shields.io/pypi/dm/hebsafeharbor.svg) 
+
+## Installation
+
+To install the package, clone the repo and install all dependencies, preferably in a virtual environment:
+
+``` sh
+# Create conda env (optional)
+conda create --name hebsafeharbor python=3.8
+conda activate hebsafeharbor
+
+# Install dependencies
+pip install -r requirements.txt
+
+# (Optional) Install package locally
+pip install -e .
+
+# Download the he_ner_news_trf model used by hebsafeharbor
+pip install https://github.com/8400TheHealthNetwork/HebSpacy/releases/download/he_ner_news_trf-3.2.1/he_ner_news_trf-3.2.1-py3-none-any.whl
+```
+
+## Getting started
+
+### Python
+
+```python
+from hebsafeharbor import HebSafeHarbor
+
+hsh = HebSafeHarbor()
+
+text = """שרון לוי התאשפזה ב02.02.2012 וגרה בארלוזרוב 16 רמת גן"""
+doc = {"text": text}
+
+output = hsh([doc])
+
+print(output)
+```
+
+### Docker
+
+1. Run the following command to install and run the server and demo services:
+
+```sh
+docker-compose up -d --build
+```
+
+2. Navigate in the browser <http://localhost:8080/docs> to test the Server swagger
+3. Navigate in the browser <http://localhost:8501> to test the Demo app
diff --git a/conda_environment.yml b/conda_environment.yml
new file mode 100644
index 0000000..4f8cef7
--- /dev/null
+++ b/conda_environment.yml
@@ -0,0 +1,9 @@
+channels:
+  - defaults
+  - conda-forge
+dependencies:
+  - python=3.8
+  - pip
+  - pip:
+    - keyring
+    - artifacts-keyring
\ No newline at end of file
diff --git a/demo/Dockerfile b/demo/Dockerfile
new file mode 100644
index 0000000..9435edc
--- /dev/null
+++ b/demo/Dockerfile
@@ -0,0 +1,27 @@
+# using two stage build to save space, based on https://pythonspeed.com/articles/multi-stage-docker-python/
+
+FROM python:3.9-slim AS compile-image
+
+WORKDIR /usr/src/app
+RUN apt-get update
+RUN apt-get install -y --no-install-recommends build-essential gcc
+RUN python3 -m venv /opt/venv
+# Make sure we use the virtualenv:
+ENV PATH="/opt/venv/bin:$PATH"
+
+COPY requirements_streamlit.txt ./
+RUN python3 -m pip install --upgrade pip
+RUN python3 -m pip install -r requirements_streamlit.txt
+
+
+FROM python:3.9-slim AS build-image
+WORKDIR /usr/src/app
+COPY main_app.py ./
+COPY utils.py ./
+COPY visualizer.py ./
+COPY --from=compile-image /opt/venv /opt/venv
+# Make sure we use the virtualenv:
+ENV PATH="/opt/venv/bin:$PATH"
+#run app and expose port 8501 for listening
+EXPOSE 8501
+ENTRYPOINT ["streamlit","run", "main_app.py"]
diff --git a/demo/main_app.py b/demo/main_app.py
new file mode 100644
index 0000000..9362058
--- /dev/null
+++ b/demo/main_app.py
@@ -0,0 +1,57 @@
+import json
+
+import requests
+import streamlit as st
+
+st.set_page_config(layout="wide", page_title="Hebrew Safe Harbor", initial_sidebar_state="collapsed")
+
+from visualizer import visualize_response
+
+DEFAULT_TEXT = "גדעון לבנה הגיע היום לבית החולים שערי צדק עם תלונות על כאבים בחזה"
+PATH = r"../resources_old"
+URL = "http://server:8080"
+URL = "http://localhost:8080"
+
+def run():
+    st.title("Hebrew Safe Harbor demo application")
+    # TODO check if server is ready + create url
+
+    with st.form("analyze text"):
+        text = st.text_area("Enter text to analyze", DEFAULT_TEXT)
+        req_data = {
+            "docs": [
+                {
+                    "id": "doc_1",
+                    "text": text
+                }
+            ]
+        }
+
+        submitted = st.form_submit_button("Analyze")
+
+        if submitted:
+            if text is not None and len(text) > 0:
+                response = requests.post(f"{URL}/query", json=req_data)
+                content = json.loads(response.content)
+
+                with st.expander("JSON response"):
+                    response_content = json.dumps(content, indent=4, ensure_ascii=False)
+                    st.code(response_content, language="json")
+
+                docs = content.get("docs", [])
+                if len(docs) == 1:
+                    doc = docs[0]
+                    visualize_response(text, doc)
+                else: # TODO handle server connectivity issues better...
+                    if response.status_code == 20:  # response consists of more than one doc
+                        st.error("Expected a single text to process and visualize, but received more than one")
+                    else:
+                        response = requests.post(f"{URL}/ready", json=req_data)
+                        if response.status_code == 503:
+                            st.error("Server is not ready - Try again soon or check server")
+                        else:
+                            st.error(response.text)
+
+
+if __name__ == "__main__":
+    run()
diff --git a/demo/requirements_streamlit.txt b/demo/requirements_streamlit.txt
new file mode 100644
index 0000000..2d3365d
--- /dev/null
+++ b/demo/requirements_streamlit.txt
@@ -0,0 +1,3 @@
+streamlit==1.4.0
+spacy==3.2.1
+pandas==1.4.0
\ No newline at end of file
diff --git a/demo/utils.py b/demo/utils.py
new file mode 100644
index 0000000..9fc4473
--- /dev/null
+++ b/demo/utils.py
@@ -0,0 +1,101 @@
+import streamlit as st
+from spacy.displacy import EntityRenderer
+
+HTML_WRAPPER = """<div style="dir: auto; overflow-x: auto; border: 1px solid #e6e9ef; border-radius: 0.25rem; padding: 1rem; margin-bottom: 2.5rem">{}</div>"""
+
+entity_colors = {
+    "CREDIT_CARD": "#FFF8C3",
+    "CRYPTO": "#CBC3FF",
+    "DATE_TIME": "#00cdcd",
+    "DOMAIN_NAME": "#bc8f8f",
+    "EMAIL_ADDRESS": "#4682b4",
+    "IBAN_CODE": "#808080",
+    "IP_ADDRESS": "#ffe4b5",
+    "NRP": "#6666ff",
+    "LOCATION": "#ee76ff",
+    "PERSON": "#cdff76",
+    "PHONE_NUMBER": "#daa520",
+    "MEDICAL_LICENSE": "#66cd00",
+    "US_BANK_NUMBER": "#f0e68c",
+    "US_DRIVER_LICENSE": "#ffaeb9",
+    "US_ITIN": "#FFC3C3",
+    "US_PASSPORT": "#dcdcdc",
+    "US_SSN": "#B49DDC",
+    "UK_NHS": "#C59DDC",
+    "NIF": "#ff4500",
+    "FIN/NRIC": "#7fff00",
+    "AU_ABN": "#ff8c00",
+    "AU_ACN": "#424242",
+    "AU_TFN": "#ff1493",
+    "AU_MEDICARE": "#228b22",
+    "PERS": "#1e90ff",
+
+    "CARDINAL": "#8bddff",
+    "DATE": "#ffabcd",
+    "EVENT": "#68228b",
+    "FAC": "#ff69b4",
+    "GPE": "#7fffd4",
+    "LANGUAGE": "#8b8b00",
+    "LAW": "#d8bfd8",
+    "LOC": "#FDC9A6",
+    "MONEY": "#8b7355",
+    "NORP": "#fafa88",
+    "ORDINAL": "#00bfff",
+    "ORG": "#ff00ff",
+    "PERCENT": "#008000",
+    "XXX": "#dc143c",
+    "PRODUCT": "#deb887",
+    "QUANTITY": "#add8e6",
+    "TIME": "#da70d6",
+    "WORK_OF_ART": "#00cc8c",
+
+    "FOOD": "#ff6c6c",
+    "EXTERNAL_EVENT": "#666680",
+    "CARE_ENVIRONMENT": "#123456",
+    "HEALTHCARE_PROFESSIONAL": "#ffd700",
+    "ADMINISTRATIVE_EVENT": "#bfccff",
+    "NAME": "#4169e1",
+    "ADDRESS": "#cdcd00",
+    "YYY": "#db7093",
+    "PHONE_OR_FAX": "#ff7f50",
+    "ID": "#8b0000",
+    "EMAIL": "#76ee00",
+    "URL": "#ffff00",
+    "ZZZ": "#4d4d4d"
+}
+
+other_colors = {
+    "FOLLOWING_NEGATIONS": "#FFAA00",
+    "PRECEDING_NEGATIONS": "#AAFF00",
+    "PSUDEO_NEGATIONS": "#FF00AA",
+    "TERMINATION": "#CCCCFF",
+    "SEC": "#fbe2e5",
+    "SEGMENTER": "#bedbbb",
+    "LINEBREAK_CLASSIFIER": "#ddf3f5",
+    "ONE_LINE_SECTION": "#ddf3f5",
+    "MERGING": "#ccf6c8",
+    "NOT_MERGING": "#c3aed6"
+}
+
+colors = {**entity_colors, **other_colors}
+
+
+def render_entities(doc_dict, options=None):
+    _html = {}
+    tpl_ent = """
+    <mark class="entity" style="background: {bg}; padding: 0.45em 0.45em; margin: 0 0.25em; line-height: 1; border-radius: 1.00em; border-style: solid; border-color: {bg}; opacity: 0.5;">
+        {text}
+        <span style="font-size: 0.8em; font-weight: bold; line-height: 1; border-radius: 0.35em; text-transform: uppercase; vertical-align: middle; margin-left: 0.5rem">{label}</span>
+    </mark>
+    """
+    if options is None:
+        options = {"colors": colors}
+    renderer = EntityRenderer(options=options)
+    doc_dict.update({"settings": {"direction": "rtl"}})
+    _html["parsed"] = renderer.render([doc_dict]).strip()
+    html = _html["parsed"]
+    # Newlines seem to mess with the rendering
+    html = html.replace("⏎", "<br>")
+    html = html.replace("\n", " ")
+
+    st.write(HTML_WRAPPER.format(html), unsafe_allow_html=True)
diff --git a/demo/visualizer.py b/demo/visualizer.py
new file mode 100644
index 0000000..ae62f40
--- /dev/null
+++ b/demo/visualizer.py
@@ -0,0 +1,27 @@
+from typing import Dict, List
+
+import pandas as pd
+import streamlit as st
+
+from utils import render_entities
+
+
+def show_entities(text: str, items: pd.DataFrame):
+    spans = [{"start": item["startPosition"], "end": item["endPosition"], "label": item["entityType"]} for _, item in
+             items.iterrows()]
+    doc_dict = {"text": text, "ents": spans}
+    render_entities(doc_dict=doc_dict)
+
+
+def visualize_response(source_text: str, response: Dict):
+    items_df = pd.DataFrame.from_records(response.get("items"))
+
+    identification_df = items_df[["textStartPosition", "textEndPosition", "textEntityType"]]
+    identification_df.columns = ["startPosition", "endPosition", "entityType"]
+    show_entities(source_text, identification_df)
+
+    anonymized_df = items_df[["maskStartPosition", "maskEndPosition", "textEntityType"]]
+    anonymized_df.columns = ["startPosition", "endPosition", "entityType"]
+    show_entities(response["text"], anonymized_df)
+
+    st.table(items_df)
\ No newline at end of file
diff --git a/docker-compose.yml b/docker-compose.yml
new file mode 100644
index 0000000..95905c8
--- /dev/null
+++ b/docker-compose.yml
@@ -0,0 +1,14 @@
+version: '3'
+
+services:
+  demo:
+    build: demo
+    ports:
+      - 8501:8501
+    depends_on:
+      - server
+
+  server:
+    build: .
+    ports:
+      - 8080:8080
diff --git a/hebsafeharbor/__init__.py b/hebsafeharbor/__init__.py
new file mode 100644
index 0000000..9ac6b96
--- /dev/null
+++ b/hebsafeharbor/__init__.py
@@ -0,0 +1,4 @@
+from hebsafeharbor.common.document import Doc
+from hebsafeharbor.manager import HebSafeHarbor
+
+__all__ = ["Doc", "HebSafeHarbor"]
diff --git a/hebsafeharbor/anonymizer/__init__.py b/hebsafeharbor/anonymizer/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/hebsafeharbor/anonymizer/custom_operators/__init__.py b/hebsafeharbor/anonymizer/custom_operators/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/hebsafeharbor/anonymizer/custom_operators/birth_date_anonymizer_operator.py b/hebsafeharbor/anonymizer/custom_operators/birth_date_anonymizer_operator.py
new file mode 100644
index 0000000..74cf00c
--- /dev/null
+++ b/hebsafeharbor/anonymizer/custom_operators/birth_date_anonymizer_operator.py
@@ -0,0 +1,98 @@
+from datetime import date
+from typing import Dict
+
+from presidio_anonymizer.operators import OperatorType, Operator
+
+from hebsafeharbor.anonymizer.setting import DAY_MASK, MONTH_MASK, YEAR_MASK
+from hebsafeharbor.common.date_utils import extract_date_components, DateMention
+import re
+
+class BirthDateAnonymizerOperator(Operator):
+    """
+    An instance of the DateAnonymizerOperator which extends the Operator abstract class (@Presidio). For each recognized
+    entity of type BIRTH_DATE, this custom operator anonymizes the day and the month.
+    """
+
+    def operate(self, text: str, params: Dict = None) -> str:
+        """
+        This method applies the anonymization policy of the BirthDateAnonymizerOperator on the given recognized entity text
+
+        :param text: recognized entity text for anonymization
+        :param params: optional parameters
+        :return: the anonymized text of the entity
+        """        
+        date_container = extract_date_components(text)
+            
+        # in case that the components extraction failed, all values will be none - returning the original text
+        if date_container.day is None and date_container.month is None and date_container.year is None:
+            return text
+
+        delta = BirthDateAnonymizerOperator.calculate_estimated_age(date_container)
+        above_89 = delta.days / 365.0 >= 89
+        less_than_year = delta.days < 365
+
+        # masking
+        if date_container.day and date_container.day.text:
+            date_container.day.text = DAY_MASK
+        if date_container.month and date_container.month.text:
+            date_container.month.text = MONTH_MASK if not less_than_year else date_container.month.text
+        if date_container.year and date_container.year.text:
+            date_container.year.text = YEAR_MASK if above_89 else date_container.year.text
+        return date_container.reconstruct_date_string()
+
+    def validate(self, params: Dict = None) -> None:
+        """
+        This method validates each operator parameters
+        :param params: operator custom parameters
+        """
+        pass
+
+    def operator_name(self) -> str:
+        """
+        Returns the operator name
+
+        :return: the operator name
+        """
+
+        return "replace_day_month"
+
+    def operator_type(self) -> OperatorType:
+        """
+        Returns the operator type
+
+        :return: the operator type
+        """
+
+        return OperatorType.Anonymize
+
+    @staticmethod
+    def calculate_estimated_age(date_mention: DateMention) -> int:
+        """
+        Helper function that calculates estimated age of the patient based on the difference between the given birth
+        date and date of today.
+        The function only supprts numerical dates. For other type of date formats (Hebrew or Latin) it will return a default of 50
+
+        :param date_mention: the mention of the birth date
+        :return: the estimated age of the patient
+        """
+        
+        current_date = date.today()
+        
+        if (date_mention.month.text.isdigit()) and (date_mention.day.text.isdigit()) and (date_mention.year.text.isdigit()):
+            # Year treatment: if given a 4 digits year (e.g 1996), use it as is
+            # for 2 digit years (i.e 96), the century will be determined using the following rule:
+            # if the number formed by the last two digits of the current year >= the given year, 
+            # assign the current century. otherwise, assign the previous century
+            if len(date_mention.year.text) == 4:
+                int_year = int(date_mention.year.text)
+            elif int(date_mention.year.text) <= int(str(current_date.year)[-2:]):
+                int_year =  round(current_date.year,-2) + int(date_mention.year.text)
+            else:
+                int_year =  round(current_date.year,-2) - 100 + int(date_mention.year.text)
+
+            birth_date = date(int_year, int(date_mention.month.text), int(date_mention.day.text))
+        else:
+            birth_date =  date(date.today().year-50, date.today().month,date.today().day)
+
+        delta = current_date - birth_date
+        return delta
diff --git a/hebsafeharbor/anonymizer/custom_operators/country_anonymizer_operator.py b/hebsafeharbor/anonymizer/custom_operators/country_anonymizer_operator.py
new file mode 100644
index 0000000..8970493
--- /dev/null
+++ b/hebsafeharbor/anonymizer/custom_operators/country_anonymizer_operator.py
@@ -0,0 +1,50 @@
+from typing import Dict
+
+from presidio_anonymizer.operators import OperatorType, Operator
+
+from hebsafeharbor.common.country_utils import COUNTRY_DICT
+
+class CountryAnonymizerOperator(Operator):
+    """
+    An instance of the CountryAnonymizerOperator which extends the Operator abstract class (@Presidio). For each recognized
+    entity of type COUNTRY, this custom operator anonymizes the country according to the regions mapping.
+    """
+
+    def operate(self, text: str, params: Dict = None) -> str:
+        """
+        This method applies the anonymization policy of the CountryAnonymizerOperator on the given recognized entity text
+
+        :param text: recognized entity text for anonymization
+        :param params: optional parameters
+        :return: the anonymized text of the entity
+        """
+
+        if text in COUNTRY_DICT.keys():
+            return COUNTRY_DICT[text]
+        else:
+            return "<מדינה_>"
+
+    def validate(self, params: Dict = None) -> None:
+        """
+        This method validates each operator parameters
+        :param params: operator custom parameters
+        """
+        pass
+
+    def operator_name(self) -> str:
+        """
+        Returns the operator name
+
+        :return: the operator name
+        """
+
+        return "replace_by_region"
+
+    def operator_type(self) -> OperatorType:
+        """
+        Returns the operator type
+
+        :return: the operator type
+        """
+
+        return OperatorType.Anonymize
diff --git a/hebsafeharbor/anonymizer/custom_operators/hebrew_replace_anonymizer_operator.py b/hebsafeharbor/anonymizer/custom_operators/hebrew_replace_anonymizer_operator.py
new file mode 100644
index 0000000..4a433fb
--- /dev/null
+++ b/hebsafeharbor/anonymizer/custom_operators/hebrew_replace_anonymizer_operator.py
@@ -0,0 +1,38 @@
+from typing import Dict
+
+from presidio_anonymizer.operators import Operator, OperatorType
+
+class ReplaceInHebrew(Operator):
+    """
+    An instance of the Operator abstract class (@Presidio). For each recognized entity ,this custom
+    replace it by custom string depending on its entity type.
+    """
+
+    def operate(self, text: str = None, params: Dict = None) -> str:
+        """:return: new_value."""
+        entity_type = params.get("entity_type")
+        if entity_type in ["PERS", "PER"]:
+            return "<שם_>"
+        elif entity_type in ["LOC", "GPE"]:
+            return "<מיקום_>"
+        elif entity_type in ["ORG", "FAC"]:
+            return "<ארגון_>"
+        elif entity_type in ["CREDIT_CARD", "ISRAELI_ID_NUMBER", "ID"]:
+            return "<מזהה_>"
+        elif entity_type in ["EMAIL_ADDRESS", "IP_ADDRESS", "PHONE_NUMBER"]:
+            return "<קשר_>"
+        elif entity_type in ["DATE"]:
+            return "<תאריך_>"
+
+
+    def validate(self, params: Dict = None) -> None:
+        """Validate the new value is string."""
+        pass
+
+    def operator_name(self) -> str:
+        """Return operator name."""
+        return "replace_in_hebrew"
+
+    def operator_type(self) -> OperatorType:
+        """Return operator type."""
+        return OperatorType.Anonymize
diff --git a/hebsafeharbor/anonymizer/custom_operators/israeli_city_anonymizer_operator.py b/hebsafeharbor/anonymizer/custom_operators/israeli_city_anonymizer_operator.py
new file mode 100644
index 0000000..2fa713b
--- /dev/null
+++ b/hebsafeharbor/anonymizer/custom_operators/israeli_city_anonymizer_operator.py
@@ -0,0 +1,52 @@
+from typing import Dict
+
+from presidio_anonymizer.operators import OperatorType, Operator
+
+from hebsafeharbor.common.city_utils import ABOVE_THRESHOLD_CITIES_LIST, BELOW_THRESHOLD_CITIES_LIST
+
+class IsraeliCityAnonymizerOperator(Operator):
+    """
+    An instance of the IsraeliCityAnonymizerOperator which extends the Operator abstract class (@Presidio).
+    For each recognized entity of type CITY, this custom operator anonymizes the cities with population below 2000.
+    """
+
+    def operate(self, text: str, params: Dict = None) -> str:
+        """
+        This method applies the anonymization policy of the IsraeliCityAnonymizerOperator on the given recognized entity text
+
+        :param text: recognized entity text for anonymization
+        :param params: optional parameters
+        :return: the anonymized text of the entity
+        """
+
+        if text in ABOVE_THRESHOLD_CITIES_LIST:
+            return text
+        elif text in BELOW_THRESHOLD_CITIES_LIST:
+            return "<מיקום_>"
+        else:
+            return "<מיקום_>"
+
+    def validate(self, params: Dict = None) -> None:
+        """
+        This method validates each operator parameters
+        :param params: operator custom parameters
+        """
+        pass
+
+    def operator_name(self) -> str:
+        """
+        Returns the operator name
+
+        :return: the operator name
+        """
+
+        return "replace_cities_under_2k"
+
+    def operator_type(self) -> OperatorType:
+        """
+        Returns the operator type
+
+        :return: the operator type
+        """
+
+        return OperatorType.Anonymize
diff --git a/hebsafeharbor/anonymizer/custom_operators/medical_date_anonymizer_operator.py b/hebsafeharbor/anonymizer/custom_operators/medical_date_anonymizer_operator.py
new file mode 100644
index 0000000..d244a48
--- /dev/null
+++ b/hebsafeharbor/anonymizer/custom_operators/medical_date_anonymizer_operator.py
@@ -0,0 +1,57 @@
+from typing import Dict
+
+from presidio_anonymizer.operators import OperatorType, Operator
+
+from hebsafeharbor.anonymizer.setting import DAY_MASK
+from hebsafeharbor.common.date_utils import extract_date_components
+
+
+class MedicalDateAnonymizerOperator(Operator):
+    """
+    An instance of the DateAnonymizerOperator which extends the Operator abstract class (@Presidio). For each recognized
+    entity of type MEDICAL_DATE, this custom operator anonymizes the only the day.
+    """
+
+    def operate(self, text: str, params: Dict = None) -> str:
+        """
+        This method applies the anonymization policy of the BirthDateAnonymizerOperator on the given recognized entity text
+
+        :param text: recognized entity text for anonymization
+        :param params: optional parameters
+        :return: the anonymized text of the entity
+        """
+        
+        date_container = extract_date_components(text)
+        # in case that the components extraction failed, all values will be none - returning the original text
+        if date_container.day is None and date_container.month is None and date_container.year is None:
+            return text
+
+        # masking
+        if date_container.day and date_container.day.text:
+            date_container.day.text = DAY_MASK
+        return date_container.reconstruct_date_string()
+
+    def validate(self, params: Dict = None) -> None:
+        """
+        This method validates each operator parameters
+        :param params: operator custom parameters
+        """
+        pass
+
+    def operator_name(self) -> str:
+        """
+        Returns the operator name
+
+        :return: the operator name
+        """
+
+        return "replace_only_day"
+
+    def operator_type(self) -> OperatorType:
+        """
+        Returns the operator type
+
+        :return: the operator type
+        """
+
+        return OperatorType.Anonymize
diff --git a/hebsafeharbor/anonymizer/phi_anonymizer.py b/hebsafeharbor/anonymizer/phi_anonymizer.py
new file mode 100644
index 0000000..9c328c2
--- /dev/null
+++ b/hebsafeharbor/anonymizer/phi_anonymizer.py
@@ -0,0 +1,92 @@
+from typing import List
+
+from presidio_anonymizer import AnonymizerEngine
+from presidio_anonymizer.entities import OperatorConfig
+from presidio_anonymizer.operators import Operator
+
+from hebsafeharbor import Doc
+from hebsafeharbor.anonymizer.custom_operators.birth_date_anonymizer_operator import BirthDateAnonymizerOperator
+from hebsafeharbor.anonymizer.custom_operators.country_anonymizer_operator import CountryAnonymizerOperator
+from hebsafeharbor.anonymizer.custom_operators.hebrew_replace_anonymizer_operator import ReplaceInHebrew
+from hebsafeharbor.anonymizer.custom_operators.israeli_city_anonymizer_operator import IsraeliCityAnonymizerOperator
+from hebsafeharbor.anonymizer.custom_operators.medical_date_anonymizer_operator import MedicalDateAnonymizerOperator
+
+
+class PhiAnonymizer:
+    """
+    This class is responsible on the anonymization process. It anonymizes the entities using the AnonymizerEngine
+    (@Presidio)
+    """
+
+    def __init__(self):
+        """
+        Initializes the PhiAnonymizer which is composed of Presidio anonymizer and a list of custom operators*
+        """
+        self.anonymizer = AnonymizerEngine()
+        self.operators = self.init_custom_operators()
+
+    def __call__(self, doc: Doc) -> Doc:
+        """
+        This method anonymizes the entities which were previously recognized earlier by the identifier
+
+        :param doc: Doc object which holds the input text for PHI reduction and the entities which were previously
+        recognized earlier by the identifier
+        :return: an updated Doc object that contains the anonymized text (the text itself and the anonymized entities)
+        """
+        # the call to the anonymizer in the long term (custom operator is part of presidio package)
+        # anonymized_results = self.anonymizer.anonymize(text=doc.text, analyzer_results=doc.consolidated_results)
+
+        # the call to the anonymizer in the short term (custom operator as lambda function)
+        anonymized_results = self.anonymizer.anonymize(
+            text=doc.text,
+            analyzer_results=doc.granular_analyzer_results,
+            operators={
+                "PERS": OperatorConfig(self.operators[0].operator_name(),
+                                       {"lambda": lambda x, y: self.operators[0].operate(x, y)}),
+                "PER": OperatorConfig(self.operators[0].operator_name(),
+                                      {"lambda": lambda x, y: self.operators[0].operate(x, y)}),
+                "LOC": OperatorConfig(self.operators[0].operator_name(),
+                                      {"lambda": lambda x, y: self.operators[0].operate(x, y)}),
+                "GPE": OperatorConfig(self.operators[0].operator_name(),
+                                      {"lambda": lambda x, y: self.operators[0].operate(x, y)}),
+                "ORG": OperatorConfig(self.operators[0].operator_name(),
+                                      {"lambda": lambda x, y: self.operators[0].operate(x, y)}),
+                "FAC": OperatorConfig(self.operators[0].operator_name(),
+                                      {"lambda": lambda x, y: self.operators[0].operate(x, y)}),
+                "CREDIT_CARD": OperatorConfig(self.operators[0].operator_name(),
+                                              {"lambda": lambda x, y: self.operators[0].operate(x, y)}),
+                "ISRAELI_ID_NUMBER": OperatorConfig(self.operators[0].operator_name(),
+                                                    {"lambda": lambda x, y: self.operators[0].operate(x, y)}),
+                "ID": OperatorConfig(self.operators[0].operator_name(),
+                                     {"lambda": lambda x, y: self.operators[0].operate(x, y)}),
+                "EMAIL_ADDRESS": OperatorConfig(self.operators[0].operator_name(),
+                                                {"lambda": lambda x, y: self.operators[0].operate(x, y)}),
+                "IP_ADDRESS": OperatorConfig(self.operators[0].operator_name(),
+                                             {"lambda": lambda x, y: self.operators[0].operate(x, y)}),
+                "PHONE_NUMBER": OperatorConfig(self.operators[0].operator_name(),
+                                               {"lambda": lambda x, y: self.operators[0].operate(x, y)}),
+                "DATE": OperatorConfig(self.operators[0].operator_name(),
+                                       {"lambda": lambda x, y: self.operators[0].operate(x, y)}),
+                "BIRTH_DATE": OperatorConfig(self.operators[1].operator_name(),
+                                             {"lambda": lambda x: self.operators[1].operate(x)}),
+                "MEDICAL_DATE": OperatorConfig(self.operators[2].operator_name(),
+                                               {"lambda": lambda x: self.operators[2].operate(x)}),
+                "COUNTRY": OperatorConfig(self.operators[3].operator_name(),
+                                               {"lambda": lambda x: self.operators[3].operate(x)}),
+                "CITY": OperatorConfig(self.operators[4].operator_name(),
+                                          {"lambda": lambda x: self.operators[4].operate(x)}),
+            },
+        )
+        anonymized_results.items.sort(key=lambda res: res.start)
+        doc.anonymized_text = anonymized_results
+
+        return doc
+
+    def init_custom_operators(self) -> List[Operator]:
+        """
+        Creates the instances of custom operators to use during the anonymization process
+        :return: list of initialized custom operators
+        """
+        return [ReplaceInHebrew(), BirthDateAnonymizerOperator(), MedicalDateAnonymizerOperator(),
+                CountryAnonymizerOperator(), IsraeliCityAnonymizerOperator(),
+                ]
diff --git a/hebsafeharbor/anonymizer/setting.py b/hebsafeharbor/anonymizer/setting.py
new file mode 100644
index 0000000..f0394e8
--- /dev/null
+++ b/hebsafeharbor/anonymizer/setting.py
@@ -0,0 +1,5 @@
+# masks
+
+DAY_MASK = "<יום_>"
+MONTH_MASK = "<חודש_>"
+YEAR_MASK = "<שנה_>"
\ No newline at end of file
diff --git a/hebsafeharbor/common/__init__.py b/hebsafeharbor/common/__init__.py
new file mode 100644
index 0000000..4b7a071
--- /dev/null
+++ b/hebsafeharbor/common/__init__.py
@@ -0,0 +1,3 @@
+from .document import Doc
+
+__all__ = ["Doc"]
diff --git a/hebsafeharbor/common/city_utils.py b/hebsafeharbor/common/city_utils.py
new file mode 100644
index 0000000..1e4f358
--- /dev/null
+++ b/hebsafeharbor/common/city_utils.py
@@ -0,0 +1,1483 @@
+ABOVE_THRESHOLD_CITIES_LIST = [
+	"גבע בנימין",
+	"ג'לג'וליה",
+	"קרני שומרון",
+	"סאג'ור",
+	"לפיד",
+	"כפר האורנים",
+	"מעלות-תרשיחא",
+	"רמת ישי",
+	"אבו קורינאת",
+	"נווה דניאל",
+	"מוקייבלה",
+	"קרית ים",
+	"ברקת",
+	"אום אל-פחם",
+	"חולון",
+	"שבלי",
+	"אריאל",
+	"בת ים",
+	"גבעת אלה",
+	"יקיר",
+	"קרית מלאכי",
+	"רעננה",
+	"קודייראת א",
+	"נצרת",
+	"אל סייד",
+	"מעיליא",
+	"הרצליה",
+	"עכו",
+	"אטרש שבט",
+	"שגב-שלום",
+	"ג'דיידה-מכר",
+	"תפרח",
+	"בוקעאתא",
+	"ג'סר א-זרקא",
+	"חשמונאים",
+	"פסוטה",
+	"שגב",
+	"סעוה",
+	"כעביה",
+	"שלום",
+	"בית חשמונאי",
+	"גרביה",
+	"זרזיר",
+	"כפר יאסיף",
+	"ג'ת",
+	"אחוזת ברק",
+	"ניין",
+	"גבעת עדה",
+	"מסעדה",
+	"ביר אל",
+	"כעביה-טבאש-חג'אג'רה",
+	"הושעיה",
+	"בית שאן",
+	"דאלית אל-כרמל",
+	"כפר אדומים",
+	"הר אדר",
+	"הוואשלה שבט",
+	"אילת",
+	"נס ציונה",
+	"אעבלין",
+	"כפר סבא",
+	"קדומים",
+	"כפר ורדים",
+	"אעצם",
+	"רבבה",
+	"ירכא",
+	"דייר אל",
+	"באר יעקב",
+	"אבני חפץ",
+	"מסעודין אל",
+	"באקה אל-גרביה",
+	"מצפה רמון",
+	"בועיינה",
+	"תל שבע",
+	"קרית עקרון",
+	"כפר קרע",
+	"שתולים",
+	"אבו גוש",
+	"גדרה",
+	"אבו ג'ווייעד שבט",
+	"ג'ולס",
+	"ראש פינה",
+	"נאעורה",
+	"גבעת זאב",
+	"בוקייעה",
+	"קרית ביאליק",
+	"כרום",
+	"חג'אג'רה",
+	"רעות",
+	"עספיא",
+	"כסיפה",
+	"שער חפר",
+	"פרדס חנה-כרכור",
+	"כפר תבור",
+	"צור הדסה",
+	"בענה",
+	"ראשון לציון",
+	"כוכב יעקב",
+	"ברוכין",
+	"גבעת ברנר",
+	"כוכב יאיר",
+	"מבשרת ציון",
+	"מרכז שפירא",
+	"ביר אל-מכסור",
+	"עתלית",
+	"חורה",
+	"עפולה",
+	"בנימינה-גבעת עדה",
+	"גבעתיים",
+	"רמלה",
+	"פרדס חנה",
+	"יהוד",
+	"עילוט",
+	"בית יצחק-שער חפר",
+	"דימונה",
+	"נוקדים",
+	"גבעת שמואל",
+	"כרכור",
+	"אפרת",
+	"הוואשלה",
+	"יפיע",
+	"באקה אל",
+	"סייד",
+	"ג'דיידה",
+	"תקוע",
+	"עוזייר",
+	"פקיעין",
+	"היג'א",
+	"קרית שמונה",
+	"כפר כנא",
+	"מעגן מיכאל",
+	"מסעודין אל-עזאזמה",
+	"תל אביב",
+	"עץ אפרים",
+	"ג'סר א",
+	"כפר יונה",
+	"עמנואל",
+	"פחם",
+	"זמר",
+	"אור יהודה",
+	"עין מאהל",
+	"טייבה",
+	"יהוד-מונוסון",
+	"שייח' דנון",
+	"אלון שבות",
+	"אבו רובייעה",
+	"עלי זהב",
+	"ירוחם",
+	"מייסר",
+	"פרדסיה",
+	"דייר אל-אסד",
+	"שלומי",
+	"שערי תקווה",
+	"ערערה",
+	"קצר א",
+	"חצור הגלילית",
+	"מכסור",
+	"סייד שבט",
+	"סח'נין",
+	"נתיבות",
+	"ירושלים",
+	"עדי",
+	"מגדל העמק",
+	"כפר חב\"ד",
+	"קרית מוצקין",
+	"חורפיש",
+	"פתח תקווה",
+	"מג'דל שמס",
+	"יד בנימין",
+	"כאוכב אבו אל-היג'א",
+	"בנגב",
+	"נוף הגליל",
+	"קרית טבעון",
+	"מעלה עירון",
+	"מג'ד אל-כרום",
+	"טמרה",
+	"קלנסווה",
+	"נהריה",
+	"צופים",
+	"רהט",
+	"קרית ארבע",
+	"אורנית",
+	"אבו תלול",
+	"ראש העין",
+	"צאנע",
+	"נוג'ידאת",
+	"חריש",
+	"כפר כמא",
+	"סר",
+	"מעלות",
+	"בית ג'ן",
+	"כסרא",
+	"סולם",
+	"שבלי - אום אל-גנם",
+	"עין נקובא",
+	"נעלה",
+	"זנגריה",
+	"באר גנים",
+	"יבנאל",
+	"באר שבע",
+	"כרמל",
+	"טלמון",
+	"קיסריה",
+	"עיילבון",
+	"גוש חלב",
+	"צפת",
+	"ערד",
+	"עלי",
+	"אכסאל",
+	"מג'ד אל",
+	"אשקלון",
+	"כוכב השחר",
+	"ג'ש",
+	"אום בטין",
+	"קודייראת א-צאנעשבט",
+	"קרית אתא",
+	"מודיעין עילית",
+	"רמת גן",
+	"בנימינה",
+	"כאוכב אבו אל",
+	"צורן",
+	"מודיעין",
+	"מגאר",
+	"בועיינה-נוג'ידאת",
+	"אסד",
+	"גנם",
+	"עין קנייא",
+	"אבו רוקייק",
+	"עופרה",
+	"בסמת טבעון",
+	"אזור",
+	"נופית",
+	"בית אל",
+	"תל מונד",
+	"אבטין",
+	"טבאש",
+	"מיתר",
+	"זרקא",
+	"יצהר",
+	"אור עקיבא",
+	"רכסים",
+	"הוזייל",
+	"נשר",
+	"שפרעם",
+	"אבו רוקייק שבט",
+	"כפר קאסם",
+	"ע'ג'ר",
+	"ברכה",
+	"צור יצחק",
+	"קדימה-צורן",
+	"עראבה",
+	"מזרעה",
+	"אעצם שבט",
+	"דייר חנא",
+	"גבעת אבני",
+	"אבו קורינאת שבט",
+	"טייבה בעמק",
+	"בית דגן",
+	"טובא",
+	"לקיה",
+	"בת חפר",
+	"ביתר עילית",
+	"כסרא-סמיע",
+	"רחובות",
+	"שדרות",
+	"כפר מנדא",
+	"טובא-זנגריה",
+	"ביר הדאג'",
+	"אבו רובייעה שבט",
+	"בני עי\"ש",
+	"גני מודיעין",
+	"קדימה",
+	"אום אל",
+	"כאבול",
+	"פקיעין בוקייעה",
+	"פסגות",
+	"טירת כרמל",
+	"יאנוח",
+	"קרית אונו",
+	"זכרון יעקב",
+	"ברקן",
+	"שעב",
+	"שמשית",
+	"ג'ש גוש חלב",
+	"כפר שמריהו",
+	"אבו ג'ווייעד",
+	"סלמה",
+	"רמת השרון",
+	"אלעד",
+	"עזאזמה",
+	"בני ברק",
+	"טורעאן",
+	"קרית גת",
+	"חדרה",
+	"גן יבנה",
+	"אשדוד",
+	"גן נר",
+	"בסמ\"ה",
+	"אלקנה",
+	"אלעזר",
+	"צור משה",
+	"דאלית אל",
+	"קציר",
+	"מצפה יריחו",
+	"אליכין",
+	"יפו",
+	"כרמיאל",
+	"אופקים",
+	"תל אביב - יפו",
+	"חיפה",
+	"שילה",
+	"ערערה-בנגב",
+	"נתניה",
+	"קצר א-סר",
+	"שוהם",
+	"בית יצחק",
+	"בעמק",
+	"מזכרת בתיה",
+	"מגדל",
+	"מתן",
+	"דבוריה",
+	"מכר",
+	"גני תקווה",
+	"אלפי מנשה",
+	"קצרין",
+	"מודיעין-מכבים-רעות",
+	"טירה",
+	"יבנה",
+	"אבן יהודה",
+	"מונוסון",
+	"מכבים",
+	"יקנעם עילית",
+	"עומר",
+	"בית אריה",
+	"משהד",
+	"פדואל",
+	"מבוא חורון",
+	"פוריידיס",
+	"ראמה",
+	"כפר מצר",
+	"שבט",
+	"לוד",
+	"קרית יערים",
+	"מעלה אדומים",
+	"בית שמש",
+	"כפר ברא",
+	"יאנוח-ג'ת",
+	"תרשיחא",
+	"הוד השרון",
+	"הוזייל שבט",
+	"סביון",
+	"אבו סנאן",
+	"נחף",
+	"נוף איילון",
+	"ריינה",
+	"סמיע",
+	"להבים",
+	"רומת הייב",
+	"ניצן",
+	"אטרש",
+	"טבריה",
+]
+BELOW_THRESHOLD_CITIES_LIST = [
+	"ספיר",
+	"כפר אחים",
+	"מבוא דותן",
+	"ורד יריחו",
+	"בית העמק",
+	"באר טוביה",
+	"כפר חיטים",
+	"תראבין א-צאנעישוב",
+	"ורדון",
+	"בני נצרים",
+	"גן השומרון",
+	"שחרות",
+	"כפר חיים",
+	"דפנה",
+	"גרופית",
+	"לפידות",
+	"גבולות",
+	"בית שקמה",
+	"עין הבשור",
+	"מסלול",
+	"נערן",
+	"עין אל-אסד",
+	"מורן",
+	"רמת צבי",
+	"בית עובד",
+	"קורנית",
+	"מסד",
+	"רמת רזיאל",
+	"מולדת",
+	"און",
+	"עין זיוון",
+	"דלתון",
+	"כפר נוער",
+	"יפעת",
+	"מעלה החמישה",
+	"גשר",
+	"עין יעקב",
+	"חמרה",
+	"זוהר",
+	"גאליה",
+	"רינתיה",
+	"שלווה במדבר",
+	"חגי",
+	"לכיש",
+	"גבעת שמש",
+	"משמר איילון",
+	"כפר ברוך",
+	"תימורים",
+	"חרב לאת",
+	"בית זית",
+	"בני עטרות",
+	"עין",
+	"נגבה",
+	"עין דור",
+	"קוממיות",
+	"ברקאי",
+	"נווה ימין",
+	"תל יוסף",
+	"נצר חזני",
+	"נגוהות",
+	"אבו עמרה שבט",
+	"נווה עובד",
+	"אל-עזי",
+	"עין הוד",
+	"עזריה",
+	"מאיר שפיה",
+	"ריחן",
+	"שרונה",
+	"יהל",
+	"ניר ח\"ן",
+	"עוקבי בנו עוקבה",
+	"מצפה שלם",
+	"גבעת חיים מאוחד",
+	"הגושרים",
+	"כפר סאלד",
+	"כפר בלום",
+	"גנות",
+	"סגולה",
+	"זמרת",
+	"משמר הירדן",
+	"פוריה",
+	"מתת",
+	"תושיה",
+	"רוויה",
+	"בארותיים",
+	"צורית",
+	"אבו עמאר",
+	"אליפלט",
+	"נאות הכיכר",
+	"גלעד",
+	"שדמה",
+	"שורש",
+	"רבדים",
+	"בית חנניה",
+	"כפר גלעדי",
+	"נחליאל",
+	"עמיחי",
+	"חספין",
+	"נירית",
+	"נחשונים",
+	"קבועה שבט",
+	"בית הלל",
+	"כפר הנגיד",
+	"מדרשת בן גוריון",
+	"פוריה עילית",
+	"ניר ישראל",
+	"אבו עמאר שבט",
+	"חפצי-בה",
+	"חמרייה",
+	"מרחב עם",
+	"כפר חרוב",
+	"גבע כרמל",
+	"לבנים",
+	"אורנים",
+	"אדמית",
+	"אורים",
+	"צוקי ים",
+	"סנסנה",
+	"חוקוק",
+	"גבעת כ\"ח",
+	"עין המפרץ",
+	"שפיר",
+	"אחווה",
+	"נווה ים",
+	"איתנים",
+	"צפריה",
+	"הראל",
+	"טל-אל",
+	"טירת צבי",
+	"נירים",
+	"מצדות יהודה",
+	"מחנה יהודית",
+	"מעלה גמלא",
+	"כפר רופין",
+	"כפר מנחם",
+	"גדות",
+	"נתיב הגדוד",
+	"שומרה",
+	"עוצם",
+	"כפר הס",
+	"יפית",
+	"שובה",
+	"גן שמואל",
+	"שקף",
+	"אום אל-קוטוף",
+	"משמר דוד",
+	"קדר",
+	"זכריה",
+	"תלמי אלעזר",
+	"בית יהושע",
+	"אחיסמך",
+	"געש",
+	"כנות",
+	"ניר בנים",
+	"גיתית",
+	"אושה",
+	"לוזית",
+	"פקיעין חדשה",
+	"אורה",
+	"מי עמי",
+	"רמת מגשימים",
+	"מכמורת",
+	"עראמשה",
+	"משמר הנגב",
+	"דייר ראפאת",
+	"מחנה טלי",
+	"אמנון",
+	"שבי דרום",
+	"נחלים",
+	"גן שורק",
+	"נעורים",
+	"גיבתון",
+	"גינתון",
+	"דברת",
+	"ניר צבי",
+	"אחיטוב",
+	"אייל",
+	"כפר דניאל",
+	"כפר יהושע",
+	"סואעד חמרייה",
+	"לי",
+	"אלון הגליל",
+	"חרשים",
+	"אבנת",
+	"כפר בן נון",
+	"כפר גליקסון",
+	"רמות מנשה",
+	"ניצני סיני",
+	"יובלים",
+	"גן יאשיה",
+	"עין השופט",
+	"אשדות יעקב",
+	"נטור",
+	"שדה אליהו",
+	"ציפורי",
+	"עין גדי",
+	"אבשלום",
+	"מרחביה מושב",
+	"כרם יבנה",
+	"יסודות",
+	"בת עין",
+	"קוואעין שבט",
+	"בן עמי",
+	"נווה אילן",
+	"תלם",
+	"בית אלפא",
+	"רשפים",
+	"אשרת",
+	"מעלה עמוס",
+	"אדירים",
+	"מנרה",
+	"אלישיב",
+	"שדה עוזיהו",
+	"רמת דוד",
+	"נחלה",
+	"לוטן",
+	"כפר עציון",
+	"תאשור",
+	"ארגמן",
+	"אדורה",
+	"איתמר",
+	"חד-נס",
+	"שדה אילן",
+	"הדר עם",
+	"יפתח",
+	"בית יוסף",
+	"יושיביה",
+	"נווה ירק",
+	"רביד",
+	"נחושה",
+	"עזריקם",
+	"נהורה",
+	"תלמי יפה",
+	"שדמות דבורה",
+	"בארות יצחק",
+	"קדרון",
+	"אומן",
+	"קרית יעריםמוסד",
+	"שדה דוד",
+	"בית גמליאל",
+	"הבונים",
+	"הרדוף",
+	"ניר יפה",
+	"בניה",
+	"כרם בן שמן",
+	"ח'ואלד שבט",
+	"סלעית",
+	"ברעם",
+	"אשכולות",
+	"יודפת",
+	"מחנה מרים",
+	"תל עדשים",
+	"כפר עבודה",
+	"מדרך עוז",
+	"הילה",
+	"כפר המכבי",
+	"יציץ",
+	"פתחיה",
+	"חד",
+	"כחלה",
+	"יקום",
+	"עינת",
+	"יעד",
+	"בית ינאי",
+	"אבן יצחק",
+	"גבעולים",
+	"חופית",
+	"שני",
+	"בני דרור",
+	"גילון",
+	"צרעה",
+	"גאולי תימן",
+	"אבו עבדון",
+	"איתן",
+	"ניר יצחק",
+	"מפלסים",
+	"כפר קיש",
+	"נחם",
+	"ניצני עוז",
+	"רומאנה",
+	"נחשולים",
+	"יד מרדכי",
+	"טמרה יזרעאל",
+	"קדרים",
+	"אביחיל",
+	"ניצנה קהילת חינוך",
+	"מבטחים",
+	"אביאל",
+	"אומץ",
+	"חוג'ייראת ד'הרה",
+	"מחנה יוכבד",
+	"תדהר",
+	"נועם",
+	"שדה בוקר",
+	"כנרת",
+	"עזוז",
+	"חבצלת השרון",
+	"רוחמה",
+	"אילניה",
+	"נתיב העשרה",
+	"מחנה הילה",
+	"משואה",
+	"עולש",
+	"כפר מרדכי",
+	"שער הגולן",
+	"שריד",
+	"באר אורה",
+	"עין כרם-בי\"ס חקלאי",
+	"חלוץ",
+	"שואבה",
+	"ראס אל",
+	"כנרת קבוצה",
+	"שיזף",
+	"חניאל",
+	"אתגר",
+	"כפר זיתים",
+	"דבורה",
+	"חוג'ייראת",
+	"מעלה לבונה",
+	"חולתה",
+	"גבת",
+	"קרית ענבים",
+	"ברק",
+	"צופית",
+	"גת קיבוץ",
+	"חמאם",
+	"אשל הנשיא",
+	"כמהין",
+	"דוב\"ב",
+	"ענב",
+	"יתד",
+	"ניר עציון",
+	"מירב",
+	"ביריה",
+	"גבעת שפירא",
+	"רמות השבים",
+	"שלווה",
+	"משמרות",
+	"גלאון",
+	"רגבים",
+	"לוחמי הגיטאות",
+	"החותרים",
+	"פרי גן",
+	"מצפה אילן",
+	"נטף",
+	"עין חצבה",
+	"אביטל",
+	"שבי שומרון",
+	"בר יוחאי",
+	"מירון",
+	"רתמים",
+	"בני דקלים",
+	"אשלים",
+	"כפר חנניה",
+	"אמירים",
+	"הזורעים",
+	"עין העמק",
+	"גדעונה",
+	"צופר",
+	"משמר השרון",
+	"עין ראפה",
+	"שפיים",
+	"בית אורן",
+	"כרכום",
+	"מבוא חמה",
+	"צוריאל",
+	"שדה ניצן",
+	"גברעם",
+	"שומריה",
+	"עין החורש",
+	"יזרעאל",
+	"רמת הכובש",
+	"עטרת",
+	"חירות",
+	"נאות סמדר",
+	"נווה",
+	"עטאוונה שבט",
+	"הזורע",
+	"עין גב",
+	"אחיהוד",
+	"כפר נטר",
+	"רעים",
+	"תומר",
+	"קלחים",
+	"גלעד אבן יצחק",
+	"מגשימים",
+	"תלמי אליהו",
+	"שדה יואב",
+	"אלמוג",
+	"גפן",
+	"עין ורד",
+	"רמות נפתלי",
+	"רגבה",
+	"אספר",
+	"כרי דשא",
+	"גת",
+	"ישרש",
+	"ישעי",
+	"יסעור",
+	"ג'נאביב שבט",
+	"שאר ישוב",
+	"בני ציון",
+	"עזר",
+	"בית הלוי",
+	"עין כרמל",
+	"תראבין א-צאנע שבט",
+	"בית נחמיה",
+	"יסוד המעלה",
+	"עופר",
+	"כפר עזה",
+	"זיתן",
+	"מבואות ים",
+	"פורת",
+	"שריגים לי-און",
+	"ניל\"י",
+	"בטחה",
+	"מנוף",
+	"ארבל",
+	"ניצנים",
+	"חוסן",
+	"כוכב מיכאל",
+	"בית חורון",
+	"צביה",
+	"נווה חריף",
+	"מגדלים",
+	"רמות מאיר",
+	"בן שמן מושב",
+	"בוסתן הגליל",
+	"מגדים",
+	"קבועה",
+	"עזריאל",
+	"אל-עריאן",
+	"מאוחד",
+	"כפר חושן",
+	"בקעות",
+	"תלמים",
+	"פצאל",
+	"מבואות יריחו",
+	"אמציה",
+	"גמזו",
+	"משמר העמק",
+	"אשבול",
+	"מרום גולן",
+	"ינון",
+	"אשדות יעקב מאוחד",
+	"קדמת צבי",
+	"כרמי יוסף",
+	"גבעת חיים",
+	"כפר מונש",
+	"עלמון",
+	"מכמנים",
+	"ייט\"ב",
+	"נופים",
+	"נווה אטי\"ב",
+	"בת הדר",
+	"שמרת",
+	"מעברות",
+	"עין חוד",
+	"זרחיה",
+	"כפר רוזנואלד זרעית",
+	"בני יהודה",
+	"מנשית זבדה",
+	"מגדל עוז",
+	"מלכיה",
+	"דבירה",
+	"כפר ביאליק",
+	"ביצרון",
+	"מאור",
+	"הר עמשא",
+	"מעונה",
+	"עמיעוז",
+	"רמת רחל",
+	"חורשים",
+	"ניר גלים",
+	"חרמש",
+	"תנובות",
+	"יבול",
+	"יד השמונה",
+	"דן",
+	"גדיש",
+	"שער העמקים",
+	"צנדלה",
+	"אפיניש",
+	"כמון",
+	"ביתן אהרן",
+	"טמרה",
+	"כפר רוזנואלד",
+	"משכיות",
+	"נס הרים",
+	"דליה",
+	"מעין צבי",
+	"דגניה א'",
+	"קהילת חינוך",
+	"מורשת",
+	"מוצא עילית",
+	"שמיר",
+	"בארי",
+	"דורות",
+	"חצבה",
+	"צור נתן",
+	"הר גילה",
+	"משאבי שדה",
+	"יגור",
+	"בית רבן",
+	"בית הערבה",
+	"אפיניש שבט",
+	"חוסנייה",
+	"זרעית",
+	"שפר",
+	"האון",
+	"בה",
+	"חנתון",
+	"אבני איתן",
+	"קלע",
+	"עוקבי",
+	"צאנע",
+	"גונן",
+	"סופה",
+	"קרית נטפים",
+	"איילת השחר",
+	"אלוני יצחק",
+	"שחר",
+	"בית השיטה",
+	"מעגן",
+	"יראון",
+	"גזר",
+	"כפר אביב",
+	"כפר הנוער הדתי",
+	"רם",
+	"לביא",
+	"יד נתן",
+	"אוהד",
+	"להבות הבשן",
+	"בית שערים",
+	"נווה זוהר",
+	"מבקיעים",
+	"נווה אור",
+	"נעמ\"ה",
+	"גיאה",
+	"לוטם",
+	"כרמי צור",
+	"סואעד",
+	"משואות יצחק",
+	"שדי חמד",
+	"אשתאול",
+	"להב",
+	"גבעת יערים",
+	"גורנות הגליל",
+	"כדורי",
+	"כפר ידידיה",
+	"תעוז",
+	"משגב עם",
+	"אבו קרינאת יישוב",
+	"גבעת ח\"ן",
+	"מחסיה",
+	"מעוז חיים",
+	"בן שמן",
+	"גילת",
+	"שבי ציון",
+	"חצב",
+	"אשדות יעקב איחוד",
+	"אלומה",
+	"מעין ברוך",
+	"נצאצרה",
+	"רותם",
+	"נווה מיכאל",
+	"שוקדה",
+	"קוטוף",
+	"גורן",
+	"כפר תפוח",
+	"שיטים",
+	"גבעות בר",
+	"רימונים",
+	"מושב",
+	"נורדיה",
+	"סער",
+	"עלומים",
+	"מסילות",
+	"מסילת ציון",
+	"רום",
+	"אבו סריחאן",
+	"הסוללים",
+	"אבו סריחאן שבט",
+	"דמיידה",
+	"רשפון",
+	"גשר הזיו",
+	"מחניים",
+	"יכיני",
+	"קוואעין",
+	"ג'נאביב",
+	"כרמל",
+	"עתניאל",
+	"חמדיה",
+	"תירוש",
+	"שניר",
+	"יד רמב\"ם",
+	"חגור",
+	"מחנה יתיר",
+	"נמרוד",
+	"חלץ",
+	"גבעתי",
+	"לבון",
+	"מתתיהו",
+	"כרם בן זמרה",
+	"מצפה נטופה",
+	"כפר ראש הנקרה",
+	"סתריה",
+	"גיזו",
+	"נטעים",
+	"שעלבים",
+	"תראבין א",
+	"דחי",
+	"מחנה יפה",
+	"מלילות",
+	"אבטליון",
+	"כפר הרי\"ף",
+	"גבעת ישעיהו",
+	"אבו עבדון שבט",
+	"בית עזרא",
+	"חיננית",
+	"רווחה",
+	"עין אל",
+	"שעל",
+	"מעון",
+	"ד'הרה",
+	"אביבים",
+	"בית צבי",
+	"ישע",
+	"עין חרוד",
+	"אלקוש",
+	"בית זרע",
+	"שושנת העמקים",
+	"עמיר",
+	"נבטים",
+	"קטורה",
+	"צוחר",
+	"נווה זיו",
+	"בית חלקיה",
+	"דריג'את",
+	"אלון מורה",
+	"גבעת ניל\"י",
+	"צרופה",
+	"עמיקם",
+	"נחשון",
+	"כנף",
+	"מישר",
+	"שקד",
+	"עלמה",
+	"עוזה",
+	"בן זכאי",
+	"גיניגר",
+	"כפר ורבורג",
+	"עדנים",
+	"סמר",
+	"בית לחם הגלילית",
+	"פעמי תש\"ז",
+	"גן שלמה",
+	"מכחול",
+	"חצור-אשדוד",
+	"תל יצחק",
+	"אודים",
+	"חברון",
+	"אבירים",
+	"מחולה",
+	"אסד",
+	"אור הנר",
+	"יטבתה",
+	"מענית",
+	"קרית שלמה",
+	"זבארגה",
+	"חרוצים",
+	"אחיעזר",
+	"גבעת עוז",
+	"איבים",
+	"כפר גלים",
+	"צפרירים",
+	"שלוחות",
+	"קדמה",
+	"שרשרת",
+	"פרזון",
+	"ניר דוד תל עמל",
+	"זנוח",
+	"נווה שלום",
+	"מעלה אפרים",
+	"מסדה",
+	"איחוד",
+	"שילת",
+	"מצפה אבי\"ב",
+	"גבעת יואב",
+	"בית אלעזרי",
+	"היוגב",
+	"תרום",
+	"כישור",
+	"ירחיב",
+	"בית זיד",
+	"צוקים",
+	"תלמי יחיאל",
+	"גני יוחנן",
+	"קשת",
+	"נופך",
+	"מחנה תל נוף",
+	"רנן",
+	"שדות ים",
+	"שדה נחמיה",
+	"נהלל",
+	"עיר אובות",
+	"שדה צבי",
+	"אירוס",
+	"מכורה",
+	"חגלה",
+	"חמד",
+	"שורשים",
+	"פדויים",
+	"כסלון",
+	"אשחר",
+	"כיסופים",
+	"כמאנה",
+	"ראס עלי",
+	"אלומות",
+	"שער מנשה",
+	"כפר רות",
+	"מבוא מודיעים",
+	"טפחות",
+	"גנות הדר",
+	"כפר ביל\"ו",
+	"יקנעם מושבה",
+	"מגידו",
+	"פדיה",
+	"פני חבר",
+	"ארסוף",
+	"כפר פינס",
+	"אביעזר",
+	"רקפת",
+	"ערוגות",
+	"שדמות מחולה",
+	"נחל עוז",
+	"אסד שבט",
+	"כפר גדעון",
+	"מנות",
+	"להבות חביבה",
+	"מרחביה",
+	"עצמון שגב",
+	"עין איילה",
+	"ניצנה",
+	"אלונים",
+	"עין שריד",
+	"גני טל",
+	"טנא",
+	"עריאן",
+	"גת רימון",
+	"אור הגנוז",
+	"ישוב",
+	"עין שמר",
+	"ירקונה",
+	"פלמחים",
+	"מגן",
+	"עגור",
+	"יחיעם",
+	"בקוע",
+	"אפיקים",
+	"שובל",
+	"ניר משה",
+	"עין צורים",
+	"יד חנה",
+	"דולב",
+	"נטע",
+	"קליה",
+	"דור",
+	"בצרה",
+	"חולית",
+	"כרם יבנה ישיבה",
+	"אליאב",
+	"חניתה",
+	"מעלה גלבוע",
+	"גינוסר",
+	"אלמגור",
+	"אלי-עד",
+	"גיתה",
+	"עיינות",
+	"פארן",
+	"רחוב",
+	"צובה",
+	"חבר",
+	"צאלים",
+	"ניצן ב'",
+	"געתון",
+	"עשרת",
+	"נתיב הל\"ה",
+	"נטועה",
+	"מרחביה קיבוץ",
+	"שדה משה",
+	"משען",
+	"כפר סילבר",
+	"עד",
+	"אניעם",
+	"מצובה",
+	"מוסד",
+	"בית חירות",
+	"שדות מיכה",
+	"ערב אל נעים",
+	"מבועים",
+	"אלי",
+	"מזרע",
+	"פוריה - כפר עבודה",
+	"אום אל",
+	"יונתן",
+	"כלנית",
+	"משמרת",
+	"יעל",
+	"תלמי יוסף",
+	"טל שחר",
+	"בנו עוקבה",
+	"בני ראם",
+	"אפיק",
+	"מקווה ישראל",
+	"בר גיורא",
+	"גבעון החדשה",
+	"ירדנה",
+	"חמדת",
+	"כנרת מושבה",
+	"חצור",
+	"מעגלים",
+	"נוב",
+	"מנוחה",
+	"יעף",
+	"רם-און",
+	"מטולה",
+	"תל קציר",
+	"לימן",
+	"עין חרוד מאוחד",
+	"כפר חסידים א'",
+	"אילות",
+	"אל-רום",
+	"אבן ספיר",
+	"אפק",
+	"יערה",
+	"עין חרוד איחוד",
+	"המעפיל",
+	"העוגן",
+	"תל עמל",
+	"טללים",
+	"חצרים",
+	"אביגדור",
+	"זרועה",
+	"בית חנן",
+	"כפר החורש",
+	"אלוני אבא",
+	"שדה ורבורג",
+	"שלומית",
+	"מדרשת רופין",
+	"קיבוץ",
+	"עין עירון",
+	"בית נקופה",
+	"ראס אל-עין",
+	"חיבת ציון",
+	"עברון",
+	"צלפון",
+	"יגל",
+	"גני הדר",
+	"תובל",
+	"רמות",
+	"פרוד",
+	"עבדון",
+	"כרם שלום",
+	"ניר עם",
+	"כרמי קטיף",
+	"ברכיה",
+	"מיטב",
+	"מזור",
+	"מטע",
+	"אלישמע",
+	"ניר דוד",
+	"מבוא ביתר",
+	"שדי אברהם",
+	"חפצי",
+	"ח'ואלד",
+	"אבן מנחם",
+	"בית רימון",
+	"ברוש",
+	"בלפוריה",
+	"נווה מבטח",
+	"עין יהב",
+	"דקל",
+	"אמונים",
+	"אבו קרינאת",
+	"אשדוד",
+	"ינוב",
+	"סעד",
+	"אל",
+	"כפר מסריק",
+	"כפר שמואל",
+	"שדי תרומות",
+	"זבארגה שבט",
+	"אדרת",
+	"בני דרום",
+	"תלמי ביל\"ו",
+	"בורגתה",
+	"הודיות",
+	"גן הדרום",
+	"משמר השבעה",
+	"שמעה",
+	"נאות גולן",
+	"בית מאיר",
+	"מרגליות",
+	"יקנעם",
+	"טל",
+	"שריגים",
+	"תמרת",
+	"נאות מרדכי",
+	"נוגה",
+	"כדיתה",
+	"בית ברל",
+	"נען",
+	"רמת השופט",
+	"שדה יצחק",
+	"עטאוונה",
+	"בית קמה",
+	"גבעת חיים איחוד",
+	"עין השלושה",
+	"שדה אליעזר",
+	"גן חיים",
+	"גבים",
+	"ראש צורים",
+	"שער אפרים",
+	"מעש",
+	"אחוזם",
+	"עידן",
+	"ריחאניה",
+	"כפר שמאי",
+	"חפץ חיים",
+	"ניר עקיבא",
+	"בצת",
+	"כפר זוהרים",
+	"פטיש",
+	"כפר מימון",
+	"הררית",
+	"דישון",
+	"שכניה",
+	"נס",
+	"כליל",
+	"אליקים",
+	"כפר הנשיא",
+	"נוה צוף",
+	"אילון",
+	"בת שלמה",
+	"ניר עוז",
+	"כפר יחזקאל",
+	"כפר סירקין",
+	"בת חן",
+	"קבוצה",
+	"כפר הרא\"ה",
+	"רביבים",
+	"אבו עמרה",
+	"עמוקה",
+	"הודיה",
+	"אלוני הבשן",
+	"ישיבה",
+	"אליפז",
+	"כברי",
+	"שתולה",
+	"סוסיה",
+	"מצר",
+	"פלך",
+	"רמת יוחנן",
+	"נווה איתן",
+	"גבע",
+	"גני עם",
+	"כפר מל\"ל",
+	"בי\"ס חקלאי",
+	"צבעון",
+	"באר מילכה",
+	"כפר חסידים ב'",
+	"מצפה",
+	"כרמיה",
+	"נורית",
+	"גבעת השלושה",
+	"זיקים",
+	"כפר אוריה",
+	"סאסא",
+	"מנחמיה",
+	"נצאצרה שבט",
+	"נצר סרני",
+	"קבוצת יבנה",
+	"נס עמים",
+	"שדה נחום",
+	"ניר אליהו",
+	"תקומה",
+	"גאולים",
+	"תל תאומים",
+	"גזית",
+	"מגל",
+	"כחל",
+	"שבט",
+	"גליל ים",
+	"מגן שאול",
+	"בית קשת",
+	"אבן שמואל",
+	"חולדה",
+	"כפר יעבץ",
+	"חדיד",
+	"עין הנצי\"ב",
+	"עמינדב",
+	"דגניה ב'",
+	"קרית יערים",
+	"כפר ויתקין",
+	"מצליח",
+	"אשבל",
+	"אורטל",
+	"גלגל",
+	"יישוב",
+	"טירת יהודה",
+	"שיבולים",
+	"עין כרם",
+	"אודם",
+	"בית ניר",
+	"בן שמן כפר נוער",
+	"מלכישוע",
+	"בחן",
+	"שזור",
+	"מעלה מכמש",
+	"ברור חיל",
+	"עמיעד",
+	"בית עוזיאל",
+	"נתיב השיירה",
+	"בית גוברין",
+	"עזי",
+	"רועי",
+	"בית הגדי",
+	"שדה יעקב",
+	"כורזים",
+	"מיצר",
+	"אורות",
+	"כרם מהר\"ל",
+	"עמקה",
+	"סואעד כמאנה שבט",
+	"בית עריף",
+	"משגב דב",
+	"גשור",
+	"מושבה",
+	"מלאה",
+	"עין תמר",
+	"זבדיאל",
+	"כפר טרומן",
+	"נווה אבות",
+	"חזון",
+	"פוריה - נווה עובד",
+	"יובל",
+	"ארז",
+	"כרמים",
+	"רחלים",
+]
+ABBREVIATIONS_LIST = ['‘עפ',
+'‘אר',
+'‘אשד',
+'‘אש',
+'ב”י',
+'ב”ש',
+'נתב”ג',
+'ב”ל',
+'ביש”א',
+'בי”ש',
+'ב”ב',
+'ע”ד',
+'יה”מ',
+'‘די',
+'‘אי',
+'אי”ש',
+'גלה”ע',
+'גלה”ת',
+'ג”ק',
+'‘חד',
+'‘חי',
+'‘הר',
+'הוד”ש',
+'‘חו',
+'חוב”ת',
+'י-מ',
+'יו”ש',
+'יש”ע',
+'‘קצ',
+'כפ”ס',
+'כ”ס',
+'ק”ג',
+'קמ”צ',
+'ק”א',
+'ק”ש',
+'לור”ם',
+'‘מס',
+'‘נה',
+'‘נצ',
+'נעמ”ה',
+'נ”ס',
+'‘נת',
+'נו”ש',
+'נוו”צ',
+'פ”ח',
+'פ”ת',
+'רכ”ס',
+'רמת”א',
+'רמא”ג',
+'רמב”ש',
+'ר”ג',
+'רמ”ג',
+'רג”ב',
+'רה”ג',
+'רמה”ג',
+'רמה”ש',
+'י”ס',
+'‘רח',
+'ראל”צ',
+'ראשל”צ',
+'רה”נ',
+'ר”פ',
+'ש”י',
+'שו”ש',
+'ת”א',
+'תה”ש',
+'‘טב',
+'ז”י',]
\ No newline at end of file
diff --git a/hebsafeharbor/common/country_utils.py b/hebsafeharbor/common/country_utils.py
new file mode 100644
index 0000000..df889bb
--- /dev/null
+++ b/hebsafeharbor/common/country_utils.py
@@ -0,0 +1,329 @@
+from typing import Dict
+
+COUNTRY_DICT: Dict[str, str] = {
+"אבחזיה": "ברית המועצות",
+"אוגנדה": "אפריקה",
+"אוזבקיסטן": "ברית המועצות",
+"אוסטריה": "אירופה",
+"אוסטרליה": "אוסטרליה ואוקיאניה",
+"אוקראינה": "ברית המועצות",
+"אורוגוואי": "אמריקה דרומית ומרכזית",
+"אזוואד": "אפריקה",
+"אזור תעלת פנמה": "אמריקה דרומית ומרכזית",
+"אזרביג'אן": "ברית המועצות",
+"אזרבידג'אן": "ברית המועצות",
+"אזרבייג'ן": "ברית המועצות",
+"איחוד האמירויות הערב": "מזרח התיכון",
+"איחוד האמירויות הערביות": "מזרח התיכון",
+"איטליה": "אירופה",
+"איי בהאמה": "אמריקה דרומית ומרכזית",
+"איי גילברט ואליס": "אוסטרליה ואוקיאניה",
+"איי הבתולה": "אמריקה דרומית ומרכזית",
+"איי ווליס ופוטונה": "אוסטרליה ואוקיאניה",
+"איי טורקס וקייקוס": "אמריקה דרומית ומרכזית",
+"איי מרשל": "אוסטרליה ואוקיאניה",
+"איי נורפולק": "אוסטרליה ואוקיאניה",
+"איי סיישל": "אפריקה",
+"איי פאלקלנד": "אמריקה דרומית ומרכזית",
+"איי פארו": "אירופה",
+"איי פיגי": "אוסטרליה ואוקיאניה",
+"איי קומורו": "אפריקה",
+"איי קוק": "אוסטרליה ואוקיאניה",
+"איי קיימן": "אמריקה דרומית ומרכזית",
+"איי קייפ וורדה": "אפריקה",
+"איי שלמה": "אוסטרליה ואוקיאניה",
+"איי שלמה הבריטיים": "אוסטרליה ואוקיאניה",
+"אינדונזיה": "מזרח רחוק",
+"אינדונסיה פיליפינים": "מזרח רחוק",
+"איסלנד": "אירופה",
+"איראן": "מזרח התיכון",
+"אירלנד": "אירופה",
+"אל סלבדור": "אמריקה דרומית ומרכזית",
+"אל סלוודור": "אמריקה דרומית ומרכזית",
+"אלבניה": "אירופה",
+"אלג'יריה": "צפון אפריקה",
+"אלגיריה": "צפון אפריקה",
+"אמירויות המפרץ הפרסי": "אסיה",
+"אנגולה": "אפריקה",
+"אנדורה": "אירופה",
+"אנטיגואה וברבודה": "אמריקה דרומית ומרכזית",
+"אסוואטיני": "אפריקה",
+"אסטוניה": "ברית המועצות",
+"אפגניסטן": "אסיה",
+"אקוודור": "אמריקה דרומית ומרכזית",
+"ארגנטינה": "אמריקה דרומית ומרכזית",
+"אריתריאה": "אפריקה",
+"ארמניה": "ברית המועצות",
+"ארצות הברית": "אמריקה צפונית",
+"אתיופיה": "אתיופיה",
+"בהוטן": "מזרח רחוק",
+"בהמס": "אמריקה דרומית ומרכזית",
+"בוטסואנה": "אפריקה",
+"בולגריה": "אירופה",
+"בוליביה": "אמריקה דרומית ומרכזית",
+"בוסניה": "אירופה",
+"בוסניה והרצגובינה": "אירופה",
+"בורונדי": "אפריקה",
+"בורמה תאילנד מאלזיה סנגפור קמבודיה ולאוס": "מזרח רחוק",
+"בורקינה פאסו": "אפריקה",
+"בחריין": "מזרח התיכון",
+"בילורוסיה": "ברית המועצות",
+"בלארוס": "ברית המועצות",
+"בלגיה": "אירופה",
+"בליז": "אמריקה דרומית ומרכזית",
+"בנגלדש": "מזרח רחוק",
+"בנין": "אפריקה",
+"בנין (דהומי)": "אפריקה",
+"ברבדוס": "אמריקה דרומית ומרכזית",
+"ברוניי": "מזרח רחוק",
+"ברזיל": "אמריקה דרומית ומרכזית",
+"בריטניה": "אירופה",
+"ברית המועצות": "אירופה",
+"ברמודה": "אמריקה דרומית ומרכזית",
+"ג'יבוטי": "אפריקה",
+"ג'מייקה": "אמריקה דרומית ומרכזית",
+"גאורגיה": "ברית המועצות",
+"גאנה": "אפריקה",
+"גבון": "אפריקה",
+"גואדאלופ": "אמריקה דרומית ומרכזית",
+"גואטמלה": "אמריקה דרומית ומרכזית",
+"גואם": "אסיה",
+"גווטאמלה": "אמריקה דרומית ומרכזית",
+"גיאנה": "אמריקה דרומית ומרכזית",
+"גיאנה הצרפתית": "אמריקה דרומית ומרכזית",
+"גיברלטר": "אירופה",
+"גינאה": "אפריקה",
+"גינאה ביסאו": "אפריקה",
+"גינאה החדשה": "אוסטרליה ואוקיאניה",
+"גינאה המשוונית": "אפריקה",
+"גמביה": "אפריקה",
+"גרוזיה": "ברית המועצות",
+"גרמניה": "אירופה",
+"גרמניה המזרחית": "אירופה",
+"גרמניה המערבית": "אירופה",
+"גרנדה": "אמריקה דרומית ומרכזית",
+"דומיניקה": "אמריקה דרומית ומרכזית",
+"דנמרק": "אירופה",
+"דנמרק איסלנד": "אירופה",
+"דרום אפריקה": "אפריקה",
+"דרום סודאן": "צפון אפריקה",
+"האיטי": "אמריקה דרומית ומרכזית",
+"האיים ההברדיים": "אירופה",
+"האיים ההברידיים": "אירופה",
+"האיים המלדיביים": "אסיה",
+"האנטיליים ההולנדים": "אמריקה דרומית ומרכזית",
+"האנטילים ההולנדים": "אמריקה דרומית ומרכזית",
+"הגדה המערבית": "אסיה",
+"הודו": "מזרח רחוק",
+"הודו פקיסטן וציילון": "מזרח רחוק",
+"הוואי": "אמריקה צפונית",
+"הולנד": "אירופה",
+"הונג קונג": "מזרח רחוק",
+"הונגריה": "אירופה",
+"הונדורס": "אמריקה דרומית ומרכזית",
+"הטריטוריה הבריטית באוקיינוס ההודי": "אסיה",
+"הממלכה המאוחדת": "אירופה",
+"הממלכה המאוחדת (אנגליה)": "אירופה",
+"הפיליפינים": "מזרח רחוק",
+"הרפ הדומיניקנית": "אמריקה דרומית ומרכזית",
+"הרפובליקה הדומיניקנית": "אמריקה דרומית ומרכזית",
+"הרפובליקה הדמוקרטית של קונגו": "אפריקה",
+"הרפובליקה הטורקית של צפון קפריסין": "מזרח התיכון",
+"הרפובליקה המרכז-אפריקאית": "אפריקה",
+"הרפובליקה הסינית": "מזרח רחוק",
+"הרפובליקה העממית של סין": "מזרח רחוק",
+"הרפובליקה הקטלאנית": "אירופה",
+"הרפובליקה של קונגו": "אפריקה",
+"ויטנאם הדרומית": "אסיה",
+"וייטנאם": "מזרח רחוק",
+"ונואטו": "אוסטרליה ואוקיאניה",
+"ונצואלה": "אמריקה דרומית ומרכזית",
+"ותיקן": "אירופה",
+"זימבאבווה": "אפריקה",
+"זימבבואה": "אפריקה",
+"זמביה": "אפריקה",
+"חבש אריתריאה סומאלי": "אפריקה",
+"חוף השנהב": "אפריקה",
+"טאיוואן": "מזרח רחוק",
+"טג'יקיסטן": "ברית המועצות",
+"טובאלו": "אוסטרליה ואוקיאניה",
+"טוגו": "אפריקה",
+"טונגה": "אוסטרליה ואוקיאניה",
+"טוניס": "אפריקה",
+"טורקיה": "מזרח התיכון",
+"טורקמניסטן": "ברית המועצות",
+"טימור הפורטוגלית": "אסיה",
+"טנגר": "אפריקה",
+"טנזניה": "אפריקה",
+"טרינידד וטובגו": "אמריקה דרומית ומרכזית",
+"יהודה ושומרון": "אסיה",
+"יוגוסלביה": "אירופה",
+"יוון": "אירופה",
+"יפן": "אוסטרליה ואוקיאניה",
+"ירדן": "מזרח התיכון",
+"ישראל": "ישראל",
+"כווית": "מזרח התיכון",
+"כורדיסטן": "אסיה",
+"כף ורדה": "אפריקה",
+"לאוס": "מזרח רחוק",
+"לבנון": "מזרח התיכון",
+"לוב": "צפון אפריקה",
+"לוקסמבורג": "אירופה",
+"לטביה": "ברית המועצות",
+"ליבריה": "אפריקה",
+"ליטא": "ברית המועצות",
+"ליכטנשטיין": "אירופה",
+"לסוטו": "אפריקה",
+"מאוריטניה": "צפון אפריקה",
+"מאוריציוס": "אפריקה",
+"מאלי": "אפריקה",
+"מדגסקר": "אפריקה",
+"מדינות חצי האי ערב": "מזרח התיכון",
+"מוזמביק": "אפריקה",
+"מולדובה": "ברית המועצות",
+"מונגוליה": "מזרח רחוק",
+"מונטנגרו": "אירופה",
+"מונסרת": "אמריקה דרומית ומרכזית",
+"מונקו": "אירופה",
+"מזרח טימור": "אסיה",
+"מיאנמר": "מזרח רחוק",
+"מיאנמר (בורמה)": "מזרח רחוק",
+"מיאנמר-בורמה": "מזרח רחוק",
+"מיקרונזיה": "אוסטרליה ואוקיאניה",
+"מלאווי": "אפריקה",
+"מלדיבים": "אסיה",
+"מלזיה": "מזרח רחוק",
+"מלטה": "אירופה",
+"מצרים": "צפון אפריקה",
+"מצרים סודאן": "צפון אפריקה",
+"מקאו": "מזרח רחוק",
+"מקדוניה": "אירופה",
+"מקדוניה הצפונית": "אירופה",
+"מקסיקו": "אמריקה צפונית",
+"מרוקו": "צפון אפריקה",
+"מרוקו הספרדית": "צפון אפריקה",
+"מרוקו טנג'יר מרוקו הצרפתית": "צפון אפריקה",
+"מרטיניק": "אמריקה דרומית ומרכזית",
+"מרכז אמריקה": "אמריקה דרומית ומרכזית",
+"נאורו": "אוסטרליה ואוקיאניה",
+"נורבגיה": "אירופה",
+"נורווגיה": "אירופה",
+"ניגר": "אפריקה",
+"ניגריה": "אפריקה",
+"ניו זילנד": "אוסטרליה ואוקיאניה",
+"ניואה": "אוסטרליה ואוקיאניה",
+"ניז'ר": "אפריקה",
+"ניקרגואה": "אמריקה דרומית ומרכזית",
+"נמיביה": "אפריקה",
+"נפאל": "מזרח רחוק",
+"סאו טומה ופרינסיפה": "אפריקה",
+"סודאן": "צפון אפריקה",
+"סווזילנד": "אפריקה",
+"סומליה": "אפריקה",
+"סוריה": "מזרח התיכון",
+"סורינאם": "אמריקה דרומית ומרכזית",
+"סט הלנה": "אפריקה",
+"סט וינסנט": "אמריקה דרומית ומרכזית",
+"סט. לוציה": "אמריקה דרומית ומרכזית",
+"סט. קיטס ונביס": "אמריקה דרומית ומרכזית",
+"סיירה לאון": "אפריקה",
+"סיירה ליאונה": "אפריקה",
+"סיישל": "אפריקה",
+"סין": "מזרח רחוק",
+"סין הונג קונג": "מזרח רחוק",
+"סינגפור": "מזרח רחוק",
+"סיקים": "מזרח רחוק",
+"סלובניה": "אירופה",
+"סלובקיה": "אירופה",
+"סמואה": "אוסטרליה ואוקיאניה",
+"סמואה האמריקנית": "אוסטרליה ואוקיאניה",
+"סמואה המערבית": "אוסטרליה ואוקיאניה",
+"סן מרינו": "אירופה",
+"סנגל": "אפריקה",
+"סנט וינסנט והגרנדינים": "אמריקה דרומית ומרכזית",
+"סנט לוסיה": "אמריקה דרומית ומרכזית",
+"סנט קיטס ונוויס": "אמריקה דרומית ומרכזית",
+"ספרד": "אירופה",
+"ספרד גיברלטר אנדורה": "אירופה",
+"סרביה": "אירופה",
+"סרי לנקה": "מזרח רחוק",
+"סרי לנקה (צילון)": "מזרח רחוק",
+"סרי לנקה-צילון": "מזרח רחוק",
+"עדן": "אסיה",
+"עומאן": "מזרח התיכון",
+"עיראק": "מזרח התיכון",
+"ערב הסעודית": "מזרח התיכון",
+"פאפואה": "אוסטרליה ואוקיאניה",
+"פולין": "אירופה",
+"פולינזיה הצרפתית": "אוסטרליה ואוקיאניה",
+"פורטו ריקו": "אמריקה דרומית ומרכזית",
+"פורטו-ריקו": "אמריקה דרומית ומרכזית",
+"פורטוגל": "אירופה",
+"פיג'י": "אוסטרליה ואוקיאניה",
+"פינלנד": "אירופה",
+"פלאו": "אוסטרליה ואוקיאניה",
+"פנמה": "אמריקה דרומית ומרכזית",
+"פפואה גינאה החדשה": "אוסטרליה ואוקיאניה",
+"פקיסטן": "אסיה",
+"פרגוואי": "אמריקה דרומית ומרכזית",
+"פרו": "אמריקה דרומית ומרכזית",
+"צ'אד": "אפריקה",
+"צ'ילה": "אמריקה דרומית ומרכזית",
+"צ'כוסלובקיה": "אירופה",
+"צ'כיה": "אירופה",
+"צ'צ'ניה": "ברית המועצות",
+"צכוסלובקיה": "אירופה",
+"צ'כוסלובקיה": "אירופה",
+"צרפת": "אירופה",
+"צרפת מונקו": "אירופה",
+"קובה": "אמריקה דרומית ומרכזית",
+"קולומביה": "אמריקה דרומית ומרכזית",
+"קומורו": "אפריקה",
+"קונגו": "אפריקה",
+"קונגו, רפ. דמוק.": "אפריקה",
+"קוסובו": "אירופה",
+"קוסטה ריקה": "אמריקה דרומית ומרכזית",
+"קוריאה": "מזרח רחוק",
+"קוריאה הדרומית": "מזרח רחוק",
+"קוריאה הצפונית": "מזרח רחוק",
+"קזחסטן": "ברית המועצות",
+"קטר": "מזרח התיכון",
+"קירג'זסטאן": "ברית המועצות",
+"קירגיזסטן": "ברית המועצות",
+"קירגיסטאן": "ברית המועצות",
+"קיריבטי": "אוסטרליה ואוקיאניה",
+"קלדוניה החדשה": "אוסטרליה ואוקיאניה",
+"קמבודיה": "מזרח רחוק",
+"קמרון": "אפריקה",
+"קנדה": "אמריקה צפונית",
+"קניה": "אפריקה",
+"קפריסין": "מזרח התיכון",
+"קרואטיה": "אירופה",
+"קריית הוותיקן": "אירופה",
+"ראוניון": "אפריקה",
+"רואנדה": "אפריקה",
+"רודזיה": "אפריקה",
+"רומניה": "אירופה",
+"רוסיה": "ברית המועצות",
+"רפ מרכז אפריקנית": "אפריקה",
+"רפובליקת עבר הקווקז": "ברית המועצות",
+"רצועת עזה וסיני": "מזרח התיכון",
+"שבדיה": "אירופה",
+"שוודיה": "אירופה",
+"שווייץ": "אירופה",
+"שוויץ ליכטנשטיין": "אירופה",
+"תאילנד": "מזרח רחוק",
+"תגיקיסטאן": "ברית המועצות",
+"תוניס": "צפון אפריקה",
+"תוניסיה": "צפון אפריקה",
+"תורכיה": "מזרח התיכון",
+"תורכמניסטאן": "ברית המועצות",
+"תימן": "מזרח התיכון",
+"תימן הדמוקרטית": "מזרח התיכון",
+"מכסיקו": "אמריקה צפונית",
+"אקואדור": "אמריקה דרומית ומרכזית",
+"בראזיל": "אמריקה דרומית ומרכזית",
+"אירן": "מזרח התיכון",
+"חבש אריתריאה סומאלי": "חבש אריתריאה סומאלי",
+"ברית המועצות": "ברית המועצות",
+}
\ No newline at end of file
diff --git a/hebsafeharbor/common/date_regex.py b/hebsafeharbor/common/date_regex.py
new file mode 100644
index 0000000..48f1c5d
--- /dev/null
+++ b/hebsafeharbor/common/date_regex.py
@@ -0,0 +1,10 @@
+# Hebrew Dates
+heb_months_re = '|'.join(["אלול","אב","תמוז","סיון","סיוון","אייר","ניסן","אדר","שבט","טבת","כסלו","כיסלו","חשוון","תשרי"])
+heb_days_re = '|'.join(["[א-ל]׳","""י״(?:[א-ד]|[ז-ט])""","""ט״(?:ו|ז)""","""כ״[א-ט]""","""ל״א"""])
+heb_year_re = r"ה?ת[א-ת][א-ת]?״[א-ת]"
+HEB_DATE_REGEX =  rf"\b(?:ב|ל|מ|מה|ה)?({heb_days_re})?(?:\s+)?(?:ב|ל|בחודש|לחודש)?(?:-|\s)?({heb_months_re})(?:\s|,\s)?({heb_year_re})?\b"
+
+# Latin Dates
+latin_months_re =    '|'.join(["ינואר","פברואר","מרץ","אפריל","מאי","יוני","יולי","אוגוסט","ספטמבר","אוקטובר","נובמבר","דצמבר"])
+num_days_re = r"[1-9]|0[1-9]|[1-2][0-9]|3[0-1]"
+LATIN_DATE_REGEX =  rf"\b(?:ב|ל|מ|מה|ה)?({num_days_re})?(?:\s+)?(?:ב|ל|בחודש|לחודש)?(?:-|\s)?({latin_months_re})(?:\s|,\s)?(\d\d|\d\d\d\d)?\b"
\ No newline at end of file
diff --git a/hebsafeharbor/common/date_utils.py b/hebsafeharbor/common/date_utils.py
new file mode 100644
index 0000000..f7f1743
--- /dev/null
+++ b/hebsafeharbor/common/date_utils.py
@@ -0,0 +1,144 @@
+import re
+from typing import List
+from hebsafeharbor.common.date_regex import HEB_DATE_REGEX, LATIN_DATE_REGEX
+MIN_DATE_LENGTH = 6
+
+class DateMentionComponent:
+    """
+    This class stores the information on the certain date component - its value (text) and its offset the date mention
+    """
+
+    def __init__(self, text: str, start: int, end:int):
+        """
+        Initialize the DateMentionComponent
+
+        :param text: the value of the date component (its text)
+        :param start: the first index of the date component
+        :param end: the last index of the date component
+        """
+        self.text = text
+        self.start = start
+        self.end = end
+
+
+class DateMention:
+    """
+    This class stores the information on the components of a certain date - the date components and the string that
+    separates between them. In case that not all the components exist, None will be placed under their member field.
+    """
+
+    def __init__(self, day: DateMentionComponent = None, month: DateMentionComponent = None,
+                 year: DateMentionComponent = None,
+                 text: str = None):
+        """
+        Initialize the DateMention
+
+        :param day: DateComponent object which represents the day
+        :param month: DateComponent object which represents the month
+        :param year: DateComponent object which represents the year
+        :param text: the original text
+        """
+        self.day = day
+        self.month = month
+        self.year = year
+        self.text = text
+
+    def reconstruct_date_string(self) -> str:
+        """
+        Reconstructs the date string based on the stored date components information and the given string prefix
+
+        :return a string of the stored date along with the text that precedes the date
+        """
+        date_components = [self.day, self.month, self.year]
+        date_components = [dt for dt in date_components if (dt is not None) and (dt.text is not None)] #filter empty date components
+        sorted_date_components = sorted(date_components, key=lambda date_comp: date_comp.start, reverse=True)
+        masked_text = self.text
+        #move backwards on the text, replacing the components in reverse order
+        for date_comp in sorted_date_components:
+            masked_text = masked_text[:date_comp.start] + date_comp.text + masked_text[date_comp.end:]
+
+        return masked_text
+ 
+
+def set_date_mention_component(matched:re.Match,ind:int) -> DateMentionComponent:
+    '''
+    Extracts the date and its span from a regular expression and sets a DateMentionComponent 
+    instance accordingly.
+    :param matched: a re.Match instance containing the groups of a regular expression
+    :param ind: the index of the group we would like to retrieve
+    :return: a DateMentionComponent instance
+    '''
+    return DateMentionComponent(matched.group(ind),matched.span(ind)[0],matched.span(ind)[1])
+
+def set_date_mention(matched:re.Match,group_order:List[int]) -> DateMention:
+    '''
+    Extracts the date and its span from a regular expression and sets a DateMention
+    instance accordingly.
+    :param matched: a re.Match instance containing the groups of a regular expression
+    :param group_order: the order of the group indexes to extract from the matched regular expression
+    :return: a list of DateMentionComponent instances
+    '''
+    day,month,year = [set_date_mention_component(matched,ind) for ind in group_order]
+    return DateMention(day=day,month=month, year=year,text=matched.string)
+
+
+
+
+def extract_date_components(text: str) -> DateMention:
+    """
+    Extracts the different date components out of the give recognized entity text
+
+    :param text: the recognized entity text
+    :return: a DateContainer object which contains the extracted date components (text and location) - day, month and
+    year - along with the separator.
+    """
+
+    # if there are less than 6 characters, it is probably not a date (just a mistake in recognition such as season,
+    # scale of pain 3/10, etc.)
+    if len(text) < MIN_DATE_LENGTH:
+        return DateMention()
+
+    #Hebrew dates
+    heb_pattern = HEB_DATE_REGEX
+    matched = re.search(heb_pattern,text)
+    if matched:
+        return set_date_mention(matched,group_order=[1,2,3])
+        
+    #Latin dates
+    latin_pattern = LATIN_DATE_REGEX
+    matched = re.search(latin_pattern,text)
+    if matched:
+        return set_date_mention(matched,group_order=[1,2,3]) 
+   
+    # Numerical dates
+    num_pattern = r"(\d+)(?:[/.-])(\d+)(?:(?:[/.-])(\d+))?"
+    matched = re.search(num_pattern,text)
+    if matched:
+        if not matched.group(3): #date with only two components <month>[/.-]<year> or <year>[/.-]<month>
+            day = None
+            if int(matched.group(1)) > 12:
+                year = set_date_mention_component(matched,ind=1)
+                month = set_date_mention_component(matched,ind=2)
+            else:
+                month = set_date_mention_component(matched,ind=1)
+                year = set_date_mention_component(matched,ind=2)
+            return DateMention(day=day,month=month,text=text)        
+        else:
+            if int(matched.group(1))>31:        
+                year = set_date_mention(matched,ind=1)
+                if int(matched.group(2))>12:
+                    month = set_date_mention_component(matched,ind=3)
+                    day = set_date_mention_component(matched,ind=2)
+                else:
+                    month = set_date_mention_component(matched,ind=2)
+                    day = set_date_mention_component(matched,ind=3)
+            else:
+                year = set_date_mention_component(matched,ind=3)
+                if int(matched.group(2))>12:
+                    month = set_date_mention_component(matched,ind=1)
+                    day = set_date_mention_component(matched,ind=2)
+                else:
+                    month = set_date_mention_component(matched,ind=2)
+                    day = set_date_mention_component(matched,ind=1)
+            return DateMention(day=day,month=month, year=year,text=text)
+    return DateMention()
\ No newline at end of file
diff --git a/hebsafeharbor/common/document.py b/hebsafeharbor/common/document.py
new file mode 100644
index 0000000..2abbad4
--- /dev/null
+++ b/hebsafeharbor/common/document.py
@@ -0,0 +1,36 @@
+import uuid
+from typing import List, Dict
+
+from presidio_analyzer import RecognizerResult
+from presidio_anonymizer.entities import EngineResult
+
+
+class Doc:
+    """
+    This class serves as a container for the information gained as part of the entire PHI reduction process
+    (identification and anonymization)
+    """
+
+    def __init__(self, data: Dict[str, str]):
+        """
+        Doc initialization
+        :param text: the input text for the PHI reduction process
+        """
+        self.metadata = data
+        self.text = None
+        self.id = None
+        if "text" in self.metadata:
+            self.text = data["text"]
+        else:
+            message = "Could not find \'text\' in data"
+            raise ValueError(message)
+        if "id" in self.metadata:
+            self.id = str(data["id"])
+        else:
+            # create a synthetic id
+            self.id = str(uuid.uuid4())
+        self.analyzer_results: List[RecognizerResult] = []
+        self.smoothed_entities: List[RecognizerResult] = []
+        self.consolidated_results: List[RecognizerResult] = []
+        self.granular_analyzer_results: List[RecognizerResult] = []
+        self.anonymized_text: EngineResult = []
diff --git a/hebsafeharbor/common/prepositions.py b/hebsafeharbor/common/prepositions.py
new file mode 100644
index 0000000..33dddf3
--- /dev/null
+++ b/hebsafeharbor/common/prepositions.py
@@ -0,0 +1,3 @@
+LOCATION_PREPOSITIONS = ["מ", "ל", "ב", "ו"]
+DISEASE_PREPOSITIONS = ["מ", "ל", "ב", "ו"]
+MEDICATION_PREPOSITIONS = ["מ", "ל", "ב", "ו"]
diff --git a/hebsafeharbor/common/scripts/extract_israeli_cities.py b/hebsafeharbor/common/scripts/extract_israeli_cities.py
new file mode 100644
index 0000000..f0d38be
--- /dev/null
+++ b/hebsafeharbor/common/scripts/extract_israeli_cities.py
@@ -0,0 +1,84 @@
+import urllib.request
+import json
+from typing import List, Dict
+
+import numpy as np
+import pandas as pd
+import re
+
+
+def clean_items_punctuation(ds, filter_items: List[str] = list()):
+    """
+    The function removes brackets from all the column items, and for each record with brackets or dash
+    adds separate record with content within / outside brackets or each section separated by dash
+    (Ex. ['Tel Aviv - Yaffo', 'Nir David (Tel Amal)'] ->
+     ['Tel Aviv - Yaffo', 'Tel Aviv', 'Yaffo', 'Nir David (Tel Amal)', 'Nir David', 'Tel Amal'] )
+
+    Parameters
+    ----------
+    limit_records : int
+        Number of records for the API query (default is 2000). Before running an update, please check total available
+        records for the required API at https://data.gov.il/dataset/residents_in_israel_by_communities_and_age_groups
+    residents_threshold : int
+        cities are devided into 2 lists based on the residents threshold
+    """
+    clean_list = list()
+    for c in ds.values:
+        splitted_list = re.split('\)|\(|-', c)
+        if len(splitted_list) > 1:
+            c = c.replace(")", "").replace("(", "")
+            splitted_list.append(c)
+        splitted_list = [item_part.strip() for item_part in splitted_list]
+        splitted_list = [item_part.replace('"', '\\"') for item_part in splitted_list if item_part]
+        clean_list.extend(splitted_list)
+    if len(filter_items) > 0:
+        for filter_item in filter_items:
+            if filter_item in clean_list:
+                clean_list.remove(filter_item)
+    return clean_list
+
+
+def save_lists_to_file(items_to_save: Dict[str, List[str]], output_file_name: str):
+    with open(output_file_name, 'w') as f:
+        for k, v in items_to_save.items():
+            f.write(f"{k} = [\n")
+            for item in set(v):
+                f.write(f'\t"{item}",\n')
+            f.write(']\n')
+
+
+def update_cities_list(limit_records: int = 2000, residents_threshold: int = 2000) -> None:
+    """
+
+    Parameters
+    ----------
+    limit_records : int
+        Number of records for the API query (default is 2000). Before running an update, please check total available
+        records for the required API at https://data.gov.il/dataset/residents_in_israel_by_communities_and_age_groups
+    residents_threshold : int
+        cities are devided into 2 lists based on the residents threshold
+    """
+
+    URL = 'https://data.gov.il/api/3/action/datastore_search?resource_id=64edd0ee-3d5d-43ce-8562-c336c24dbc1f&limit=' + \
+          str(limit_records)
+
+    resource = urllib.request.urlopen(URL)
+    content = resource.read().decode(resource.headers.get_content_charset())
+    loaded_dict = json.loads(content)['result']['records']
+    df = pd.DataFrame.from_dict(loaded_dict)
+    df.rename(columns={"סהכ": "residents", "שם_ישוב": "city", "נפה": "county", "מועצה_אזורית": "council"}, inplace=True)
+    df['above_threshold'] = np.where(df['residents'] >= residents_threshold, 'above', 'below')
+
+    above = clean_items_punctuation(df.loc[df.above_threshold == "above", ("city")], ["לא רשום"])
+    below = clean_items_punctuation(df.loc[df.above_threshold == "below", ("city")], ["לא רשום"])
+
+    d = {
+        "ABOVE_THRESHOLD_CITIES_LIST": above,
+        "BELOW_THRESHOLD_CITIES_LIST": below,
+    }
+
+    save_lists_to_file(d, "../city_utils.py")
+
+
+if __name__ == "__main__":
+    update_cities_list(limit_records=2000, residents_threshold=2000)
diff --git a/hebsafeharbor/common/terms_recognizer.py b/hebsafeharbor/common/terms_recognizer.py
new file mode 100644
index 0000000..2cc48b1
--- /dev/null
+++ b/hebsafeharbor/common/terms_recognizer.py
@@ -0,0 +1,44 @@
+import re
+from typing import List, Tuple, Optional
+import ahocorasick
+
+
+class TermsRecognizer:
+
+    def __init__(self, phrase_list: List[str]):
+        """
+        Initializes TermsRecognizer
+        :param phrase_list: list of terms to recognize
+        """
+        self._automaton = ahocorasick.Automaton(ahocorasick.STORE_LENGTH)
+        for phrase in phrase_list:
+            self._automaton.add_word(phrase)
+        self._automaton.make_automaton()
+
+    def __call__(self, text: str, prefixes: Optional[List[str]] = None) -> List[Tuple[int, int]]:
+        """
+        This method searches for terms in text
+        :param text: text
+        :return: List of starting offsets of matches and their length
+        """
+        offsets = []
+        for end_index_short, length in self._automaton.iter(text):
+            offset = end_index_short - length + 1
+            if length == 1 or offset < 0 or offset >= len(text):
+                pass
+            else:
+                if prefixes:
+                    start_cond = offset == 0 or re.match(r"\W", text[offset - 1]) or \
+                                 re.match("|".join(prefixes), text[offset - 1])
+                else:
+                    start_cond = offset == 0 or re.match(r"\W", text[offset - 1])
+                is_phrase = start_cond \
+                            and (offset + length == len(text)
+                                 or re.match(r"\W", text[offset + length]))
+                if is_phrase:
+                    offsets.append((offset, length))
+
+        # drop duplicates
+        offsets = list(set(offsets))
+
+        return offsets
diff --git a/hebsafeharbor/identifier/__init__.py b/hebsafeharbor/identifier/__init__.py
new file mode 100644
index 0000000..a7b9b30
--- /dev/null
+++ b/hebsafeharbor/identifier/__init__.py
@@ -0,0 +1,5 @@
+from .consolidation.consolidator import NerConsolidator
+from .heb_nlp_engine import HebSpacyNlpEngine
+from .phi_identifier import PhiIdentifier
+
+__all__ = ["HebSpacyNlpEngine", "PhiIdentifier", "NerConsolidator"]
diff --git a/hebsafeharbor/identifier/consolidation/__init__.py b/hebsafeharbor/identifier/consolidation/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/hebsafeharbor/identifier/consolidation/conflict_handler.py b/hebsafeharbor/identifier/consolidation/conflict_handler.py
new file mode 100644
index 0000000..ef41aef
--- /dev/null
+++ b/hebsafeharbor/identifier/consolidation/conflict_handler.py
@@ -0,0 +1,154 @@
+from abc import ABC, abstractmethod
+from typing import List, Set
+
+from presidio_analyzer import RecognizerResult
+
+from hebsafeharbor import Doc
+from hebsafeharbor.identifier.consolidation.consolidation_config import ENTITY_TYPE_TO_CATEGORY
+from hebsafeharbor.identifier.consolidation.overlap_resolver import PreferLongestEntity, ContextBasedResolver, \
+    CategoryMajorityResolver
+
+
+class ConflictHandler(ABC):
+
+    @abstractmethod
+    def handle(self, entities: List[RecognizerResult], doc: Doc) -> RecognizerResult:
+        """
+        This function handles with the conflict by triggering the right conflict resolvers based on the conflict case
+
+        :param entities: a group of overlapped entities
+        :param doc: Doc object
+        :return: the selected entity to be kept
+        """
+        pass
+
+
+class ExactMatch(ConflictHandler):
+
+    def handle(self, entities: List[RecognizerResult], doc: Doc) -> RecognizerResult:
+        """
+        This function handles with conflict in which all the entities are from the same category and have the same
+        boundaries
+
+        :param entities: a group of overlapped entities
+        :param doc: Doc object
+        :return: the selected entity to be kept
+        """
+        # there is no real conflict here - return the first in the given entities list
+        return entities[0]
+
+
+class SameCategory(ConflictHandler):
+
+    def __init__(self, prefer_longest_entity_resolver: PreferLongestEntity):
+        self.prefer_longest_entity_resolver = prefer_longest_entity_resolver
+
+    def handle(self, entities: List[RecognizerResult], doc: Doc) -> RecognizerResult:
+        """
+        This function handles with conflict in which all the entities are from the same category but have different
+        boundaries
+
+        :param entities: a group of overlapped entities
+        :param doc: Doc object
+        :return: the selected entity to be kept
+        """
+        # in this case we will prefer the longest entity
+        return self.prefer_longest_entity_resolver(entities, doc)
+
+
+class SameBoundaries(ConflictHandler):
+
+    def __init__(self, prefer_longest_entity_resolver: PreferLongestEntity,
+                 context_based_resolver: ContextBasedResolver, category_majority_resolver: CategoryMajorityResolver):
+        self.prefer_longest_entity_resolver = prefer_longest_entity_resolver
+        self.context_based_resolver = context_based_resolver
+        self.category_majority_resolver = category_majority_resolver
+
+    def handle(self, entities: List[RecognizerResult], doc: Doc) -> RecognizerResult:
+        """
+        This function handles with conflict in which all the entities have the same boundaries but are from different
+        categories
+
+        :param entities: a group of overlapped entities
+        :param doc: Doc object
+        :return: the selected entity to be kept
+        """
+        group_categories = set(map(lambda entity: ENTITY_TYPE_TO_CATEGORY[entity.entity_type], entities))
+
+        # currently if there are more than 2 categories we take the longest entity
+        if len(group_categories) > 2:
+            return self.prefer_longest_entity_resolver(entities, doc)
+
+        if self.is_conflict_between(["ID", "CONTACT"], group_categories):
+            return self.context_based_resolver(entities, doc)
+
+        if self.is_conflict_between(["ID", "DATE"], group_categories):
+            if len(entities) > 2:
+                return self.category_majority_resolver(entities, doc)
+            else:
+                date_entity = entities[0] if ENTITY_TYPE_TO_CATEGORY[entities[0].entity_type] == "DATE" else entities[1]
+                id_entity = entities[0] if ENTITY_TYPE_TO_CATEGORY[entities[0].entity_type] == "ID" else entities[1]
+                # select the id entity in case that:
+                # 1. the id entity is a valid Israeli ID (checksum) **OR**
+                # 2. date entity was recognized by SpacyRecognizer and it is not a number
+                if id_entity.analysis_explanation.validation_result or (
+                        date_entity.analysis_explanation.recognizer == "SpacyRecognizer" and not doc.text[
+                                                                                                 date_entity.start:date_entity.end].isnumeric()):
+                    return id_entity
+                else:
+                    return date_entity
+
+        if self.is_conflict_between(["CONTACT", "DATE"], group_categories):
+            if len(entities) > 2:
+                return self.category_majority_resolver(entities, doc)
+            else:
+                date_entity = entities[0] if ENTITY_TYPE_TO_CATEGORY[entities[0].entity_type] == "DATE" else entities[1]
+                contact_entity = entities[0] if ENTITY_TYPE_TO_CATEGORY[entities[0].entity_type] == "CONTACT" else \
+                    entities[1]
+                # select the contact entity in case that the date entity recognized by SpacyRecognizer
+                if date_entity.analysis_explanation.recognizer == "SpacyRecognizer":
+                    return contact_entity
+                else:
+                    return date_entity
+
+        if self.is_conflict_between(["CONTACT", "ORG"], group_categories):
+            if len(entities) > 2:
+                return self.category_majority_resolver(entities, doc)
+            else:
+                # select the first CONTACT entity
+                for entity in entities:
+                    if ENTITY_TYPE_TO_CATEGORY[entity.entity_type] == "CONTACT":
+                        return entity
+
+        # default - take longest
+        return self.prefer_longest_entity_resolver(entities, doc)
+
+    def is_conflict_between(self, case: List[str], condition_categories: Set[str]) -> bool:
+        """
+        Helper function for identifying the whose are the categories in conflict
+
+        :param case: a list of categories that describe the case in question
+        :param condition_categories: the entities' categories in the current condition
+        :return: True if this is the case and False otherwise
+        """
+        for category in condition_categories:
+            if category not in case:
+                return False
+        return True
+
+
+class Mixed(ConflictHandler):
+    def __init__(self, prefer_longest_entity_resolver: PreferLongestEntity):
+        self.prefer_longest_entity_resolver = prefer_longest_entity_resolver
+
+    def handle(self, entities: List[RecognizerResult], doc: Doc) -> RecognizerResult:
+        """
+        This function handles with conflict in which entities are from different categories and have different
+        boundaries
+
+        :param entities: a group of overlapped entities
+        :param doc: Doc object
+        :return: the selected entity to be kept
+        """
+        # currently in this case we will prefer the longest entity
+        return self.prefer_longest_entity_resolver(entities, doc)
diff --git a/hebsafeharbor/identifier/consolidation/consolidation_config.py b/hebsafeharbor/identifier/consolidation/consolidation_config.py
new file mode 100644
index 0000000..aaa0312
--- /dev/null
+++ b/hebsafeharbor/identifier/consolidation/consolidation_config.py
@@ -0,0 +1,42 @@
+from enum import Enum
+
+ENTITY_TYPES_TO_IGNORE = {"MONEY", "PERCENT", "MISC__ENT", "MISC_EVENT", "WOA", "EVE", "DUC", "ANG"}
+
+ENTITY_TYPES_TO_POSTPROCESS = {"COUNTRY", "CITY", "DISEASE", "MEDICATION"}
+
+ENTITY_TYPE_TO_CATEGORY = {
+    "PERS": "NAME",
+    "LOC": "LOCATION",
+    "ORG": "ORG",
+    "TIME": "DATE",
+    "DATE": "DATE",
+    "MISC__AFF": "MISC__AFF",
+    "PER": "NAME",
+    "GPE": "LOCATION",
+    "FAC": "ORG",
+    "CREDIT_CARD": "ID",
+    "DATE_TIME": "DATE",
+    "EMAIL_ADDRESS": "CONTACT",
+    "IP_ADDRESS": "CONTACT",
+    "PHONE_NUMBER": "CONTACT",
+    "ISRAELI_ID_NUMBER": "ID",
+    "ID": "ID",
+    "COUNTRY": "LOCATION",
+    "CITY": "LOCATION",
+    "DISEASE": "INTERNAL",
+    "MEDICATION": "INTERNAL"
+}
+
+CATEGORY_TO_CONTEXT_PHRASES = {
+    "ID": ["תעודה", "זהות", "מזהה", "רישיון", "מ.ר", "מ.ז", "ת.ז", "מספר אישי", "רשיון", "מנוי", "עובד", "רכב", "בנק",
+           "אשראי"],
+    "CONTACT": ["מיקוד", "בטלפון", "טלפון", "טלפון נייח", "טלפון נייד", "פלאפון", "פקס", "טלפון בבית", "טלפון בעבודה",
+                "אימייל", "דואר אלקטרוני", "URL", "IP", "מייל"]
+}
+
+
+class ConflictCase(Enum):
+    EXACT_MATCH = "EXACT_MATCH"
+    SAME_CATEGORY = "SAME_CATEGORY"
+    SAME_BOUNDARIES = "SAME_BOUNDARIES"
+    MIXED = "MIXED"
diff --git a/hebsafeharbor/identifier/consolidation/consolidator.py b/hebsafeharbor/identifier/consolidation/consolidator.py
new file mode 100644
index 0000000..5593c8f
--- /dev/null
+++ b/hebsafeharbor/identifier/consolidation/consolidator.py
@@ -0,0 +1,153 @@
+import copy
+from typing import List
+
+from presidio_analyzer import RecognizerResult
+
+from hebsafeharbor.common.document import Doc
+from hebsafeharbor.identifier.consolidation.conflict_handler import ExactMatch, SameCategory, SameBoundaries, Mixed
+from hebsafeharbor.identifier.consolidation.consolidation_config import ENTITY_TYPE_TO_CATEGORY, ENTITY_TYPES_TO_IGNORE, \
+    ConflictCase, ENTITY_TYPES_TO_POSTPROCESS
+from hebsafeharbor.identifier.consolidation.post_consolidation.city_country_post_consolidator import CityCountryPostConsolidator
+from hebsafeharbor.identifier.consolidation.overlap_resolver import PreferLongestEntity, ContextBasedResolver, \
+    CategoryMajorityResolver
+from hebsafeharbor.identifier.consolidation.post_consolidation.medical_post_consolidator import MedicalPostConsolidator
+
+
+class NerConsolidator:
+    """
+    A class which is responsible for consolidating the entities recognized by the different signals to create a set of
+    entities without overlaps. It applies custom policy for prioritizing the results of the different signals.
+    """
+
+    def __init__(self):
+        self.prefer_longest_entity_resolver = PreferLongestEntity()
+        self.context_based_resolver = ContextBasedResolver()
+        self.category_majority_resolver = CategoryMajorityResolver()
+        self.conflict_handlers = {
+            ConflictCase.EXACT_MATCH: ExactMatch(),
+            ConflictCase.SAME_CATEGORY: SameCategory(self.prefer_longest_entity_resolver),
+            ConflictCase.SAME_BOUNDARIES: SameBoundaries(self.prefer_longest_entity_resolver,
+                                                         self.context_based_resolver,
+                                                         self.category_majority_resolver),
+            ConflictCase.MIXED: Mixed(self.prefer_longest_entity_resolver)
+        }
+        self.postprocess_consolidators = [CityCountryPostConsolidator(), MedicalPostConsolidator()]
+
+    def __call__(self, doc: Doc) -> Doc:
+        """
+        :param doc: a Doc object which contains the entities recognized by the different signals
+        :return: an updated Doc object that contains the consolidated entities (a set of entities with no overlaps)
+        """
+
+        recognized_entities = copy.deepcopy(doc.smoothed_entities)
+        # filter out entities from irrelevant types
+        filtered_entities = filter(
+            lambda entity: entity.entity_type not in ENTITY_TYPES_TO_IGNORE.union(ENTITY_TYPES_TO_POSTPROCESS),
+            recognized_entities)
+
+        filtered_entities = sorted(filtered_entities, key=lambda entity: (entity.start, entity.end))
+        group = self.get_next_overlapped_entities_group(filtered_entities, 0)
+        consolidated_entities = []
+        while len(group) > 0:
+            selected_entity = self.consolidate_entities(group, doc)
+            if selected_entity is None:
+                # only one entity in group and it doesn't satisfy the minimal conditions
+                last_entity = group[0]
+            else:
+                consolidated_entities.append(selected_entity)
+                last_entity = selected_entity
+            group = self.get_next_overlapped_entities_group(filtered_entities, last_entity.end)
+
+        # trigger the custom entity consolidators
+        for custom_consolidator in self.postprocess_consolidators:
+            custom_entities = filter(
+                lambda entity: entity.entity_type in custom_consolidator.supported_entity_types,
+                recognized_entities)
+            if custom_entities:
+                consolidated_entities = custom_consolidator(consolidated_entities, list(custom_entities), doc)
+
+        doc.consolidated_results = consolidated_entities
+        return doc
+
+    def get_next_overlapped_entities_group(self, entities: List[RecognizerResult], start_offset: int) -> List[
+        RecognizerResult]:
+        """
+        Helper function for getting the next group of overlapped entities
+
+        :param entities: recognized entities
+        :param start_offset: the offset to start the search from
+        :return: the next subgroup of overlapped entities (the returned list will be empty if no entities left)
+        """
+        group = []
+        not_yet_handled_entities = list(filter(lambda entity: entity.start >= start_offset, entities))
+        if len(not_yet_handled_entities) == 0:
+            return group
+        not_yet_handled_entities = sorted(not_yet_handled_entities, key=lambda entity: entity.start)
+        group.append(not_yet_handled_entities[0])
+        min_group_offset, max_group_offset = group[0].start, group[0].end
+        for entity in not_yet_handled_entities[1:]:
+            if min_group_offset <= entity.start < max_group_offset:
+                group.append(entity)
+            else:
+                return group
+        return group
+
+    def keep_single_entity(self, entity: RecognizerResult, doc: Doc) -> bool:
+        """
+        Helper function which decides whether to keep an entity that doesn't overlap any other recognized entity
+
+        :param entity: an entity that doesn't overlap any other recognized entity
+        :param doc: Doc object
+        :return True if the entity should be kept and False otherwise
+        """
+        entity_text = doc.text[entity.start: entity.end]
+        # don't keep entity if entity text contains only one character
+        if len(entity_text) < 2:
+            return False
+        # don't keep entity in case it is in English and all caps
+        if 'A' <= entity_text[0] <= 'Z' and entity_text.isalpha() and entity_text.upper() == entity_text:
+            return False
+        return True
+
+    def consolidate_entities(self, entities_in_conflict: List[RecognizerResult], doc: Doc) -> RecognizerResult:
+        """
+        This method consolidate a group of overlapped entities by identifying the conflict case and triggering the
+        relevant condition handler for resolution
+
+        :param entities_in_conflict: group of overlapped entities
+        :param doc: Doc object
+        :return: the selected entity out of the group or None in case it doesn't satisfy the minimal requirements
+        """
+
+        # if there is only one entity in group there is no conflict - keep it in case it satisfying the minimal
+        # requirements
+        if len(entities_in_conflict) == 1:
+            if self.keep_single_entity(entities_in_conflict[0], doc):
+                return entities_in_conflict[0]
+            else:
+                return None
+
+        conflict_case = NerConsolidator.infer_conflict_case(entities_in_conflict)
+        return self.conflict_handlers[conflict_case].handle(entities_in_conflict, doc)
+
+    @staticmethod
+    def infer_conflict_case(entities_in_conflict: List[RecognizerResult]) -> ConflictCase:
+        """
+        Determines the consolidation conflict case (same categories, same boundaries, etc.)
+
+        :param entities_in_conflict: group of entities with overlaps
+        :return conflict case
+        """
+        group_categories = set(map(lambda entity: ENTITY_TYPE_TO_CATEGORY[entity.entity_type], entities_in_conflict))
+        same_category = len(group_categories) == 1
+        group_start_offsets = set(map(lambda entity: entity.start, entities_in_conflict))
+        group_end_offsets = set(map(lambda entity: entity.end, entities_in_conflict))
+        same_boundaries = len(group_start_offsets) == 1 & len(group_end_offsets) == 1
+
+        if same_category & same_boundaries:
+            return ConflictCase.EXACT_MATCH
+        if same_category:
+            return ConflictCase.SAME_CATEGORY
+        if same_boundaries:
+            return ConflictCase.SAME_BOUNDARIES
+        return ConflictCase.MIXED
diff --git a/hebsafeharbor/identifier/consolidation/overlap_resolver.py b/hebsafeharbor/identifier/consolidation/overlap_resolver.py
new file mode 100644
index 0000000..31fd693
--- /dev/null
+++ b/hebsafeharbor/identifier/consolidation/overlap_resolver.py
@@ -0,0 +1,102 @@
+from abc import abstractmethod, ABC
+from collections import Counter
+from typing import List
+
+from presidio_analyzer import RecognizerResult
+
+from hebsafeharbor import Doc
+from hebsafeharbor.common.terms_recognizer import TermsRecognizer
+from hebsafeharbor.identifier.consolidation.consolidation_config import CATEGORY_TO_CONTEXT_PHRASES, ENTITY_TYPE_TO_CATEGORY
+
+
+class OverlapResolver(ABC):
+    """
+    This abstract class defines one method that every overlap resolver must implement - the function that resolves the
+    conflict between entities.
+    """
+
+    @abstractmethod
+    def __call__(self, entities_in_conflict: List[RecognizerResult], doc: Doc) -> RecognizerResult:
+        """
+        This function resolves the conflict between overlapped entities. Given a group of overlapped entities it resolve
+        the conflict (type and boundaries) and return the updated list of entities without overlaps
+
+        :param entities_in_conflict: a group of overlapped entities
+        :param doc: Doc object
+        :return: the entity that should be kept (the result of the overlap resolution)
+        """
+        pass
+
+
+class PreferLongestEntity(OverlapResolver):
+
+    def __call__(self, entities_in_conflict: List[RecognizerResult], doc: Doc) -> RecognizerResult:
+        """
+        This function resolves the conflict between overlapped entities by keeping only the longest entity
+
+        :param entities_in_conflict: a group of overlapped entities
+        :param doc: Doc object
+        :return: the longest entity
+        """
+        sorted_by_length = sorted(entities_in_conflict, key=lambda entity: entity.end - entity.start, reverse=True)
+        return sorted_by_length[0]
+
+
+class ContextBasedResolver(OverlapResolver):
+
+    def __init__(self, window_size=5):
+        """
+        Initializes ContextBasedResolver by creating the the TermsRecognizer(s) for each of the categories
+
+        :param window_size: context size limit (one side)
+        """
+        self.window_size = window_size
+        self.category_to_recognizer = {}
+        for category, context_phrases in CATEGORY_TO_CONTEXT_PHRASES.items():
+            self.category_to_recognizer[category] = TermsRecognizer(context_phrases)
+
+    def __call__(self, entities_in_conflict: List[RecognizerResult], doc: Doc) -> RecognizerResult:
+        """
+        This function resolves the conflict between overlapped entities by examining words in context that may hint on
+        the right category
+
+        :param entities_in_conflict: a group of overlapped entities
+        :param doc: Doc object
+        :return: the entity to be kept according to the context
+        """
+        categories = set(map(lambda entity: ENTITY_TYPE_TO_CATEGORY[entity.entity_type], entities_in_conflict))
+        category_to_offsets = {category: self.category_to_recognizer[category](doc.text) for category in categories}
+        category_to_end_offsets = {}
+        for category, offsets in category_to_offsets.items():
+            category_to_end_offsets[category] = list(map(lambda offset: offset[0] + offset[1] - 1, offsets))
+
+        for index, entity in enumerate(entities_in_conflict):
+            entity_category = ENTITY_TYPE_TO_CATEGORY[entity.entity_type]
+            if len(category_to_end_offsets[entity_category]) > 0 and any(
+                    entity.start - end_offset <= self.window_size for end_offset in
+                    category_to_end_offsets[entity_category]):
+                return entity
+
+        # in case that there are no indicators in context, return the longest entity
+        sort_by_length = sorted(entities_in_conflict, key=lambda entity: entity.end - entity.start, reverse=True)
+        return sort_by_length[0]
+
+
+class CategoryMajorityResolver(OverlapResolver):
+    def __call__(self, entities_in_conflict: List[RecognizerResult], doc: Doc) -> RecognizerResult:
+        """
+        This function resolves the conflict between overlapped entities based on the entities' category where the
+        selected entity is from the major category
+
+        :param entities_in_conflict: a group of overlapped entities
+        :param doc: Doc object
+        :return: the entity with the major category
+        """
+        categories = map(lambda entity: ENTITY_TYPE_TO_CATEGORY[entity.entity_type], entities_in_conflict)
+        counter = Counter(categories)
+        # the most common category is the first item in the first tuple in list
+        most_common_category = counter.most_common(1)[0][0]
+        entities_with_most_common_category = list(
+            filter(lambda entity: ENTITY_TYPE_TO_CATEGORY[entity.entity_type] == most_common_category,
+                   entities_in_conflict))
+        return entities_with_most_common_category[0]
diff --git a/hebsafeharbor/identifier/consolidation/post_consolidation/__init__.py b/hebsafeharbor/identifier/consolidation/post_consolidation/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/hebsafeharbor/identifier/consolidation/post_consolidation/city_country_post_consolidator.py b/hebsafeharbor/identifier/consolidation/post_consolidation/city_country_post_consolidator.py
new file mode 100644
index 0000000..128682f
--- /dev/null
+++ b/hebsafeharbor/identifier/consolidation/post_consolidation/city_country_post_consolidator.py
@@ -0,0 +1,71 @@
+from typing import List
+
+from presidio_analyzer import RecognizerResult
+
+from hebsafeharbor import Doc
+from hebsafeharbor.common.prepositions import LOCATION_PREPOSITIONS
+from hebsafeharbor.identifier.consolidation.post_consolidation.post_consolidator_rule import PostConsolidatorRule
+
+
+class CityCountryPostConsolidator(PostConsolidatorRule):
+    """
+    This post consolidator decides for each CITY/COUNTRY entity whether to keep it or not and adjust overlapped entities
+    if necessary.
+    """
+
+    def __init__(self):
+        """
+        Initializes CityCountryPostConsolidator
+        """
+        super().__init__(supported_entity_types=["COUNTRY", "CITY"],
+                         higher_preference_entity_types=["PERS", "PER", "ORG", "FAC"],
+                         lower_preference_entity_types=["LOC", "GPE"],
+                         prefer_other_types=False)
+
+        self.location_prepositions = LOCATION_PREPOSITIONS
+
+    def __call__(self, consolidated_entities: List[RecognizerResult], custom_entities: List[RecognizerResult],
+                 doc: Doc) -> List[RecognizerResult]:
+        """
+        This method resolves overlap that can occur between entities recognized as more generic (LOC/GPE, currently
+        recognized by HebSpacy) and more specific (COUNTRY/CITY, recognized using custom recognizers). Giving more
+        priority to the specific ones allows more flexible anonymization.
+
+        :param doc: document which stores the recognized entities
+        """
+        if len(custom_entities) == 0:
+            return consolidated_entities
+
+        consolidated_entities.extend(custom_entities)
+        consolidated_entities = self.sort_entities_by_offset_start(consolidated_entities)
+
+        for i in range(1, len(consolidated_entities)):
+            if consolidated_entities[i].start <= consolidated_entities[i-1].end:
+                if self.prioritized_entity_types.index(consolidated_entities[i].entity_type) <= \
+                        self.prioritized_entity_types.index(consolidated_entities[i-1].entity_type):
+                    if (consolidated_entities[i].start - consolidated_entities[i - 1].start) > 1 & (
+                            consolidated_entities[i - 1].end - consolidated_entities[i].end) >= 1:
+                        consolidated_entities[i].end = consolidated_entities[i].start
+                    else:
+                        consolidated_entities[i - 1].end = consolidated_entities[i].start
+                else:
+                    consolidated_entities[i].start = consolidated_entities[i - 1].end \
+                        if consolidated_entities[i - 1].end <= consolidated_entities[i].end \
+                        else consolidated_entities[i].end
+
+        # .......            | ........            | ........... -> ........... | ........... -> ....******* |
+        # ******* -> ******* |  ******* -> ******* |   ********                 |     *******                |
+
+        # ******* -> ******* | ********** -> ********** | ******** -> ********| ********  -> ********..|
+        # .......            |   .....                  |  .......            |  .........             |
+
+        for entity in consolidated_entities:
+            if (entity.entity_type in self.lower_preference_entity_types) | (
+                    entity.entity_type in self.supported_entity_types):
+                if (entity.end - entity.start) == 0:
+                        consolidated_entities.remove(entity)
+                if (entity.end - entity.start) == 1:
+                    if doc.text[entity.start:entity.end] in self.location_prepositions:
+                        consolidated_entities.remove(entity)
+
+        return consolidated_entities
\ No newline at end of file
diff --git a/hebsafeharbor/identifier/consolidation/post_consolidation/medical_post_consolidator.py b/hebsafeharbor/identifier/consolidation/post_consolidation/medical_post_consolidator.py
new file mode 100644
index 0000000..09368f3
--- /dev/null
+++ b/hebsafeharbor/identifier/consolidation/post_consolidation/medical_post_consolidator.py
@@ -0,0 +1,102 @@
+from typing import List, Dict, Set
+
+from presidio_analyzer import RecognizerResult
+
+from hebsafeharbor import Doc
+from hebsafeharbor.common.prepositions import DISEASE_PREPOSITIONS, MEDICATION_PREPOSITIONS
+from hebsafeharbor.identifier.consolidation.post_consolidation.post_consolidator_rule import PostConsolidatorRule
+
+
+class MedicalPostConsolidator(PostConsolidatorRule):
+    """
+    This post processor make sure that medication or disease won't be wrongly recognized as PHI entity
+    """
+
+    def __init__(self):
+        """
+        Initializes MedicalPostConsolidator
+        """
+        super().__init__(supported_entity_types=["DISEASE", "MEDICATION"],
+                         lower_preference_entity_types=["PERS", "PER", "ORG", "FAC"],
+                         prefer_other_types=False)
+
+        self.medical_prepositions = set(DISEASE_PREPOSITIONS + MEDICATION_PREPOSITIONS)
+
+    def __call__(self, consolidated_entities: List[RecognizerResult], custom_entities: List[RecognizerResult],
+                 doc: Doc) -> List[RecognizerResult]:
+
+        """
+        This method resolves overlap that can occur between diseases and medication with NAME and ORG entities.
+        In case of conflict the NAME/ORG entities are removed (ignored)
+
+        :param consolidated_entities: list of recognized entities after consolidation (no overlaps exist)
+        :param custom_entities: list of recognized medical entities
+        :param doc: document which stores the recognized entities
+        """
+        if len(custom_entities) == 0:
+            return consolidated_entities
+
+        offset_to_medical_entity = MedicalPostConsolidator.map_offset_to_entity(custom_entities)
+        post_consolidated_entities = []
+        for entity in consolidated_entities:
+            if entity.entity_type in self.lower_preference_entity_types:
+                overlapping_medical_entities = MedicalPostConsolidator.get_entities_in_span(offset_to_medical_entity,
+                                                                                            entity.start, entity.end)
+                if len(overlapping_medical_entities) == 0 or not any(
+                        self.is_full_overlap(entity, medical_entity, doc) for medical_entity in
+                        overlapping_medical_entities):
+                    post_consolidated_entities.append(entity)
+            else:
+                post_consolidated_entities.append(entity)
+
+        return post_consolidated_entities
+
+    def is_full_overlap(self, entity: RecognizerResult, medical_entity: RecognizerResult, doc: Doc) -> bool:
+        """
+        Checks if the medical entity fully overlap the given entity where full overlap means originally same boundaries
+        or same boundaries when illuminating the preposition
+
+        :param entity: the entity to check whether it overlaps with the given medical entity
+        :param medical_entity: the medical entity to check whether it overlaps with the given entity
+        :param doc: Doc object
+        :return: True if the medical entity fully overlaps the given entity and False otherwise
+        """
+        if medical_entity.end == entity.end and medical_entity.start == entity.start:
+            return True
+        if medical_entity.end == entity.end and medical_entity.start - 1 == entity.start and doc.text[
+            medical_entity.start - 1] in self.medical_prepositions:
+            return True
+        return False
+
+    @staticmethod
+    def map_offset_to_entity(entities: List[RecognizerResult]) -> Dict[int, RecognizerResult]:
+        """
+        Creates a mapping of an offset to the entity in this offset. Note that if an offset is not part of any entity it
+        won't be part of the mapping
+
+        :param entities: list of entities to map
+        :return: a mapping of an offset to the entity in this offset, if an offset is not part of any entity it won't be
+        part of the mapping
+        """
+        offset_to_entity = {}
+        for entity in entities:
+            for i in range(entity.start, entity.end):
+                offset_to_entity[i] = entity
+        return offset_to_entity
+
+    @staticmethod
+    def get_entities_in_span(offset_to_entity: Dict[int, RecognizerResult], start: int, end: int) -> Set[
+        RecognizerResult]:
+        """
+        Based on the given offset to entity mapping, returns the entities exist in the given span boundaries
+
+        :param offset_to_entity: a mapping of an offset to the entity in this offset
+        :param start: span's start offset
+        :param end: span's end offset
+        :return: entities within the span
+        """
+        entities = []
+        for i in range(start, end):
+            if i in offset_to_entity:
+                entities.append(offset_to_entity[i])
+        return set(entities)
diff --git a/hebsafeharbor/identifier/consolidation/post_consolidation/post_consolidator_rule.py b/hebsafeharbor/identifier/consolidation/post_consolidation/post_consolidator_rule.py
new file mode 100644
index 0000000..8f06d45
--- /dev/null
+++ b/hebsafeharbor/identifier/consolidation/post_consolidation/post_consolidator_rule.py
@@ -0,0 +1,73 @@
+from abc import ABC, abstractmethod
+from typing import List
+
+from presidio_analyzer import RecognizerResult
+
+from hebsafeharbor import Doc
+from hebsafeharbor.identifier.consolidation.consolidation_config import ENTITY_TYPE_TO_CATEGORY
+
+
+class PostConsolidatorRule(ABC):
+    """
+    Entity consolidator component responsible for consolidating recognized entities after general consolidation is
+    completed. This abstract class defines the methods that each consolidator must support.
+    """
+
+    def __init__(self, supported_entity_types: List[str] = list(), higher_preference_entity_types: List[str] = list(),
+                 lower_preference_entity_types: List[str] = list(), prefer_other_types: bool = False):
+        """
+        Initializes CustomConsolidator
+
+        :param supported_entity_types: a list of entity types that the entity consolidator supports
+        :param higher_preference_entity_types: a list of entity types that are given more preference than supported
+        :param lower_preference_entity_types: a list of entity types that are given less preference than supported
+        :param prefer_other_types: a flag that determines how to treat entities not specified above (True - give more
+        preference, False - give less preference)
+        """
+        self.supported_entity_types = supported_entity_types
+        self.higher_preference_entity_types = higher_preference_entity_types
+        self.lower_preference_entity_types = lower_preference_entity_types
+        self.prefer_other_types = prefer_other_types
+
+        self.prioritized_entity_types = self.prioritize_entity_types()
+
+    @abstractmethod
+    def __call__(self, consolidated_entities: List[RecognizerResult], custom_entities: List[RecognizerResult],
+                 doc: Doc) -> List[RecognizerResult]:
+        """
+        This method consolidates overlap between entities of supported entity types.
+        This method must be implemented by any CustomEntityConsolidator.
+
+        :param consolidated_entities: list of entities that are consolidated so far
+        :param custom_entities: full list of supported_entity_types recognized entities
+        """
+        pass
+
+    def sort_entities_by_offset_start(self, entities_list: List[RecognizerResult]) -> List[RecognizerResult]:
+        """
+        This method extracts the recognized entities that the entity consolidator supports according to their type
+
+        :param doc: list of the recognized entities so far
+        :return list of recognized entities from the type that the entity consolidator supports
+        """
+        return sorted(entities_list, key=lambda entity: (entity.start, entity.end))
+
+    def prioritize_entity_types(self) -> List[str]:
+        """
+        This method extracts the recognized entities that the entity consolidator supports according to their type
+
+        :param doc: list of the recognized entities so far
+        :return list of recognized entities from the type that the entity consolidator supports
+        """
+
+        # Create a list where all the entities are prioritized according to the settings
+        prioritized_entity_list = self.higher_preference_entity_types + self.supported_entity_types +\
+                                       self.lower_preference_entity_types
+        # Check if there are entity types that aren't specified in __init__
+        other_entity_types = list(set(ENTITY_TYPE_TO_CATEGORY.keys()) - set(prioritized_entity_list))
+        if self.prefer_other_types:
+            prioritized_entity_list = other_entity_types + prioritized_entity_list
+        else:
+            prioritized_entity_list = prioritized_entity_list + other_entity_types
+
+        return prioritized_entity_list
\ No newline at end of file
diff --git a/hebsafeharbor/identifier/entity_smoother/__init__.py b/hebsafeharbor/identifier/entity_smoother/__init__.py
new file mode 100644
index 0000000..9dbdb2b
--- /dev/null
+++ b/hebsafeharbor/identifier/entity_smoother/__init__.py
@@ -0,0 +1,5 @@
+from .location_merger_rule import LocationsMergerRule
+from .entity_smoother_rule import EntitySmootherRule
+from .entity_smoother_rule_executor import EntitySmootherRuleExecutor
+
+__all__ = ["LocationsMergerRule", "EntitySmootherRule", "EntitySmootherRuleExecutor"]
diff --git a/hebsafeharbor/identifier/entity_smoother/entity_smoother_rule.py b/hebsafeharbor/identifier/entity_smoother/entity_smoother_rule.py
new file mode 100644
index 0000000..a9bb2c9
--- /dev/null
+++ b/hebsafeharbor/identifier/entity_smoother/entity_smoother_rule.py
@@ -0,0 +1,33 @@
+from abc import ABCMeta, abstractmethod
+from typing import List, Callable
+
+from presidio_analyzer import RecognizerResult
+
+from hebsafeharbor import Doc
+
+
+class EntitySmootherRule(metaclass=ABCMeta):
+    """
+    An abstract class which represents a rule which smooths some entities according to some policy. If the requirement
+    it holds is satisfied, it performs some smoothing action.
+    """
+
+    def __init__(self, name: str, requirement: Callable[[List[RecognizerResult], Doc], bool]):
+        """
+        Initializing EntitySmootherRule by defining its name and the requirement it verifies
+        :param name: the name of the rule
+        :param requirement: the condition that must be satisfied for executing the rule
+        """
+        self.name = name
+        self.requirement = requirement
+
+    @abstractmethod
+    def __call__(self, doc: Doc) -> Doc:
+        """
+        Executes the EntityExpanderRule on a list of entities by checking if the requirement is
+        satisfied and return the updated Doc object after performing some action
+
+        :param doc: doc object
+        :return an updated Doc object after executing the rule
+        """
+        pass
diff --git a/hebsafeharbor/identifier/entity_smoother/entity_smoother_rule_executor.py b/hebsafeharbor/identifier/entity_smoother/entity_smoother_rule_executor.py
new file mode 100644
index 0000000..6fefdd0
--- /dev/null
+++ b/hebsafeharbor/identifier/entity_smoother/entity_smoother_rule_executor.py
@@ -0,0 +1,35 @@
+import copy
+
+from hebsafeharbor import Doc
+from hebsafeharbor.identifier.entity_smoother.location_merger_rule import LocationsMergerRule
+
+
+class EntitySmootherRuleExecutor:
+    """
+    Applies the entity smoothing rules over the recognized entities (stored in the input Doc object).
+    Note that after calling the executor the results are saved in smoothed_entities section in the Doc.
+    """
+
+    def __init__(self):
+        """
+        Initializing the EntitySmootherRuleExecutor
+        """
+
+        # rules
+        self.locations_merger_rule = LocationsMergerRule()
+
+    def __call__(self, doc: Doc) -> Doc:
+        """
+        Execute the different entity smoothing rules one by one where each rule performed on the output set of
+        entities of the previous one and according to some conditions.
+
+        :param doc: document object
+        :return an updated document after performing the rules
+        """
+
+        # initialize the entity smoother section in Doc with a deep copy of the recognized entities
+        doc.smoothed_entities = copy.deepcopy(doc.analyzer_results)
+
+        # apply first rule - merge two consecutive LOC entities if there is a number between them
+        doc = self.locations_merger_rule(doc)
+        return doc
diff --git a/hebsafeharbor/identifier/entity_smoother/location_merger_rule.py b/hebsafeharbor/identifier/entity_smoother/location_merger_rule.py
new file mode 100644
index 0000000..d66d9e2
--- /dev/null
+++ b/hebsafeharbor/identifier/entity_smoother/location_merger_rule.py
@@ -0,0 +1,91 @@
+import re
+from typing import List, Callable
+
+from presidio_analyzer import RecognizerResult
+
+from hebsafeharbor import Doc
+from hebsafeharbor.identifier.entity_smoother.entity_smoother_rule import EntitySmootherRule
+
+
+class LocationsMergerRule(EntitySmootherRule):
+    """
+        An abstract class which represents a rule which expands some entities according to some policy. If the
+        requirement it holds is satisfied, it performs some action.
+        """
+
+    MAXIMUM_ALLOWED_DISTANCE = 5
+
+    def __init__(self):
+        """
+        Initializing EntityExpanderRule by defining its name and the requirement it verifies
+        """
+        requirement: Callable[[List[RecognizerResult], Doc], bool] = LocationsMergerRule.merging_requirement
+        super().__init__("LocationsMergerRule", requirement)
+
+    def __call__(self, doc: Doc) -> Doc:
+        """
+        Executes the EntityExpanderRule on a list of entities by checking if the requirement is
+        satisfied and return the updated Doc object after performing some action
+
+        :param doc: doc object
+        :return an updated Doc object after executing the rule
+        """
+        # concentrating on entities recognized only be HebSpacy signal
+        heb_spacy_entities = filter(lambda entity: entity.analysis_explanation.recognizer == "HebSpacy",
+                                    doc.smoothed_entities)
+        heb_spacy_entities = sorted(heb_spacy_entities, key=lambda entity: entity.start)
+
+        entities_after_expansion = []
+        i = 0
+        while i + 1 < len(heb_spacy_entities):
+            if self.requirement([heb_spacy_entities[i], heb_spacy_entities[i + 1]], doc):
+                # merging the entities along with the number between them
+                merged_entity = heb_spacy_entities[i]
+                merged_entity.end = heb_spacy_entities[i + 1].end
+                entities_after_expansion.append(merged_entity)
+                # moving on to the next pair of entities
+                i += 2
+            else:
+                entities_after_expansion.append(heb_spacy_entities[i])
+                # the requirement is not satisfied so trying with the next entity
+                i += 1
+        if i < len(heb_spacy_entities):
+            entities_after_expansion.append(heb_spacy_entities[i])
+
+        # taking the entities which weren't recognized by HebSpacy along with the entities after performing the rule
+        # logic
+        updated_entities = list(filter(lambda entity: entity.analysis_explanation.recognizer != "HebSpacy",
+                                       doc.smoothed_entities)) + entities_after_expansion
+        updated_entities = sorted(updated_entities, key=lambda entity: entity.start)
+        doc.smoothed_entities = updated_entities
+
+        return doc
+
+    @staticmethod
+    def merging_requirement(entities: List[RecognizerResult], doc: Doc) -> bool:
+        """
+        a function which defines the requirements for entities to be merged by the LocationsMergerRule:
+        - both entities must be from type LOC
+        - entities cannot be far from each other
+        - there is an unrecognized number between them
+
+        :return True if all the requirements are satisfied and False otherwise
+        """
+
+        # check if both entities are from type LOC
+        if entities[0].entity_type != "LOC" or entities[1].entity_type != "LOC":
+            return False
+
+        # check if the entities are not far from each other
+        if entities[1].start - entities[0].end > LocationsMergerRule.MAXIMUM_ALLOWED_DISTANCE:
+            return False
+
+        # check if there is an unrecognized number between the entities
+        context_start_offset = entities[0].end + 1
+        context_end_offset = entities[1].start
+        context = doc.text[context_start_offset:context_end_offset]
+        number_regex = re.compile(r"\d+")
+        if number_regex.search(context):
+            return True
+
+        return False
diff --git a/hebsafeharbor/identifier/entity_spliters/__init__.py b/hebsafeharbor/identifier/entity_spliters/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/hebsafeharbor/identifier/entity_spliters/date_entity_splitter.py b/hebsafeharbor/identifier/entity_spliters/date_entity_splitter.py
new file mode 100644
index 0000000..57e4f28
--- /dev/null
+++ b/hebsafeharbor/identifier/entity_spliters/date_entity_splitter.py
@@ -0,0 +1,44 @@
+from hebsafeharbor import Doc
+from hebsafeharbor.common.terms_recognizer import TermsRecognizer
+from hebsafeharbor.identifier.entity_spliters.entity_splitter import EntitySplitter
+
+
+class DateEntitySplitter(EntitySplitter):
+    """
+    This entity splitter decides for each DATE / DATE_TIME entity whether it describes a birth data or a medical date
+    and updates the entity type accordingly.
+    """
+
+    BIRTH_DATE_CONTEXT = ["נולד", "נולדה", "תאריך לידה", "ת.לידה"]
+    WINDOW_SIZE = 10
+
+    def __init__(self):
+        """
+        Initializes DateEntitySplitter
+        """
+        super().__init__(supported_entity_types=["DATE", "DATE_TIME"])
+        self.birth_date_terms_recognizer = TermsRecognizer(DateEntitySplitter.BIRTH_DATE_CONTEXT)
+
+    def __call__(self, doc: Doc) -> Doc:
+        """
+        This method changes the type of any date entity into more specific value - BIRTH_DATE or MEDICAL_DATE.
+
+        :param doc: document which stores the recognized entities before and after executing the DateEntitySplitter
+        """
+        date_entities = self.filter_relevant_entities(doc)
+        if len(date_entities) == 0:
+            return doc
+
+        birth_date_offsets = self.birth_date_terms_recognizer(doc.text)
+        if len(birth_date_offsets) == 0:
+            for entity in date_entities:
+                entity.entity_type = "MEDICAL_DATE"
+        birth_date_end_offsets = list(map(lambda offset: offset[0] + offset[1] - 1, birth_date_offsets))
+
+        for entity in date_entities:
+            if any(entity.start - end_offset < DateEntitySplitter.WINDOW_SIZE for end_offset in birth_date_end_offsets):
+                entity.entity_type = "BIRTH_DATE"
+            else:
+                entity.entity_type = "MEDICAL_DATE"
+
+        return doc
\ No newline at end of file
diff --git a/hebsafeharbor/identifier/entity_spliters/entity_splitter.py b/hebsafeharbor/identifier/entity_spliters/entity_splitter.py
new file mode 100644
index 0000000..7d5decb
--- /dev/null
+++ b/hebsafeharbor/identifier/entity_spliters/entity_splitter.py
@@ -0,0 +1,42 @@
+from abc import ABC, abstractmethod
+from typing import List
+
+from presidio_analyzer import RecognizerResult
+
+from hebsafeharbor import Doc
+
+
+class EntitySplitter(ABC):
+    """
+    Entity splitter component responsible for splitting recognized entities from a given type to more specific types.
+    This abstract class defines the methods that each entity splitter must support.
+    """
+
+    def __init__(self, supported_entity_types: List[str]):
+        """
+        Initializes EntitySplitter
+
+        :param supported_entity_types: a list of entity types that the entity splitter supports
+        """
+        self.supported_entity_types = supported_entity_types
+
+    @abstractmethod
+    def __call__(self, doc: Doc) -> Doc:
+        """
+        This method split or change the type of an entity into higher granularity. It works only on the entities of
+        supported entity types. This method must be implemented by any EntitySplitter.
+
+        :param doc: document which stores the recognized entities before and after the splitting process that was done
+        so far
+        """
+        pass
+
+    def filter_relevant_entities(self, doc: Doc) -> List[RecognizerResult]:
+        """
+        This method extracts the recognized entities that the entity splitter supports according to their type
+
+        :param doc: documents which includes the recognized entities so far
+        :return list of recognized entities from the type that the entity splitter supports
+        """
+        return list(
+            filter(lambda entity: entity.entity_type in self.supported_entity_types, doc.granular_analyzer_results))
diff --git a/hebsafeharbor/identifier/entity_spliters/entity_splitter_rule_executor.py b/hebsafeharbor/identifier/entity_spliters/entity_splitter_rule_executor.py
new file mode 100644
index 0000000..c2db102
--- /dev/null
+++ b/hebsafeharbor/identifier/entity_spliters/entity_splitter_rule_executor.py
@@ -0,0 +1,35 @@
+import copy
+
+from hebsafeharbor import Doc
+from hebsafeharbor.identifier.entity_spliters.date_entity_splitter import DateEntitySplitter
+
+class EntitySplitterRuleExecutor:
+    """
+    Triggers the different entity splitters over the consolidated recognized entities (exist in the input Doc object).
+    Note that after calling the executor the entities can possibly changed
+    """
+
+    def __init__(self):
+        """
+        Initializing the EntitySplitterRuleExecutor
+        """
+
+        # rules
+        self.date_entity_splitter = DateEntitySplitter()
+
+    def __call__(self, doc: Doc) -> Doc:
+        """
+        Execute the different entity smoothing rules one by one where each rule performed on the output set of
+        entities of the previous one and according to some conditions.
+
+        :param doc: document object
+        :return an updated document after triggering the entity splitters
+        """
+
+        # initialize the entity splitter section in Doc with a deep copy of the consolidated recognized entities
+        doc.granular_analyzer_results = copy.deepcopy(doc.consolidated_results)
+
+        # trigger the first entity splitter which decides for each DATE entity whether it is BIRTH_DATE or MEDICAL_DATE
+        doc = self.date_entity_splitter(doc)
+
+        return doc
diff --git a/hebsafeharbor/identifier/heb_nlp_engine.py b/hebsafeharbor/identifier/heb_nlp_engine.py
new file mode 100644
index 0000000..3952094
--- /dev/null
+++ b/hebsafeharbor/identifier/heb_nlp_engine.py
@@ -0,0 +1,28 @@
+from typing import Optional, Dict
+
+from presidio_analyzer.nlp_engine import SpacyNlpEngine, NlpArtifacts
+from spacy.tokens import Doc
+
+
+class HebSpacyNlpEngine(SpacyNlpEngine):
+    """
+    Wrapper class for SpacyNlpEngine, for cases where lemmas are not provided by the spaCy pipeline.
+    Replaces lemmas with the original token text.
+    """
+
+    def __init__(self, models: Optional[Dict[str, str]] = None):
+        if not models:
+            models = {"he": "he_ner_news_trf"}
+        super().__init__(models=models)
+
+    def _doc_to_nlp_artifact(self, doc: Doc, language: str) -> NlpArtifacts:
+        tokens_indices = [token.idx for token in doc]
+        entities = doc.ents
+        return NlpArtifacts(
+            entities=entities,
+            tokens=doc,
+            tokens_indices=tokens_indices,
+            lemmas=[token.text for token in doc],
+            nlp_engine=self,
+            language=language,
+        )
diff --git a/hebsafeharbor/identifier/phi_identifier.py b/hebsafeharbor/identifier/phi_identifier.py
new file mode 100644
index 0000000..867865a
--- /dev/null
+++ b/hebsafeharbor/identifier/phi_identifier.py
@@ -0,0 +1,154 @@
+from typing import List
+
+from hebsafeharbor.common.city_utils import BELOW_THRESHOLD_CITIES_LIST, ABOVE_THRESHOLD_CITIES_LIST, ABBREVIATIONS_LIST
+from hebsafeharbor.common.country_utils import COUNTRY_DICT
+from hebsafeharbor.common.document import Doc
+from hebsafeharbor.common.prepositions import LOCATION_PREPOSITIONS, DISEASE_PREPOSITIONS, MEDICATION_PREPOSITIONS
+from hebsafeharbor.identifier import HebSpacyNlpEngine
+from hebsafeharbor.identifier.consolidation.consolidator import NerConsolidator
+from hebsafeharbor.identifier.entity_smoother.entity_smoother_rule_executor import EntitySmootherRuleExecutor
+from hebsafeharbor.identifier.entity_spliters.entity_splitter_rule_executor import EntitySplitterRuleExecutor
+from hebsafeharbor.identifier.signals.general_id_recognizer import GeneralIdRecognizer
+
+from hebsafeharbor.identifier.signals.heb_date_recognizer import HebDateRecognizer
+from hebsafeharbor.identifier.signals.heb_preposition_date_recognizer import PrepositionDateRecognizer
+from hebsafeharbor.identifier.signals.heb_latin_date_recognizer import HebLatinDateRecognizer
+from hebsafeharbor.identifier.signals.israeli_id_recognizer import IsraeliIdNumberRecognizer
+from presidio_analyzer import AnalyzerEngine, LocalRecognizer, RecognizerRegistry
+from presidio_analyzer.predefined_recognizers import CreditCardRecognizer, DateRecognizer, EmailRecognizer, \
+    IpRecognizer, PhoneRecognizer, SpacyRecognizer
+
+from hebsafeharbor.identifier.signals.lexicon_based_recognizer import LexiconBasedRecognizer
+from hebsafeharbor.lexicons.disease import DISEASES
+from hebsafeharbor.lexicons.medications import MEDICATIONS
+
+
+class PhiIdentifier:
+    """
+    This class is responsible on the identification process, recognize entities using the AnalyzerEngine (@Presidio) and
+    consolidate them using NERConsolidator.
+    """
+
+    def __init__(self):
+        """
+        Initializes the PhiIdentifier which is composed of Presidio analyzer and NerConsolidator
+        """
+
+        self.analyzer = self._init_presidio_analyzer()
+        self.entity_smoother = EntitySmootherRuleExecutor()
+        self.entity_splitter = EntitySplitterRuleExecutor()
+        self.consolidator = NerConsolidator()
+
+    def __call__(self, doc: Doc) -> Doc:
+        """
+        This method identifies the PHI entities
+
+        :param doc: Doc object which holds the input text for PHI reduction
+        :return: an updated Doc object that contains the the set of entities that were recognized by the different
+        signals and the consolidated set of entities
+        """
+
+        # recognition
+        analyzer_results = self.analyzer.analyze(text=doc.text, language="he", return_decision_process=True)
+        doc.analyzer_results = sorted(analyzer_results, key=lambda res: res.start)
+
+        # entity smoothing
+        doc = self.entity_smoother(doc)
+
+        # consolidation
+        doc = self.consolidator(doc)
+
+        # entity splitter
+        doc = self.entity_splitter(doc)
+
+        return doc
+
+    def _init_presidio_analyzer(self) -> AnalyzerEngine:
+        """
+        Creates and initializes the Presidio analyzer
+        :return: Presidio analyzer
+        """
+
+        # create NLP engine based on the nlp configuration
+        nlp_engine = HebSpacyNlpEngine(models={"he": "he_ner_news_trf"})
+
+        # initialize the signals
+        signals = self._init_analyzer_signals()
+        # create the signals registry
+        registry = RecognizerRegistry()
+        # add the different signals to registry
+        for signal in signals:
+            registry.add_recognizer(signal)
+
+        # create the AnalyzerEngine using the created registry, NLP engine and supported_languages
+        analyzer = AnalyzerEngine(
+            registry=registry,
+            nlp_engine=nlp_engine,
+            supported_languages=["he"]
+        )
+
+        return analyzer
+
+    def _init_analyzer_signals(self) -> List[LocalRecognizer]:
+        """
+        Creates and initializes the analyzer's NER signals
+
+        :return: a list of NER initialized signals
+        """
+
+        ner_signals = []
+
+        # presidio predefined recognizers
+        ner_signals.append(CreditCardRecognizer(supported_language="he", context=["כרטיס", "אשראי"]))
+        ner_signals.append(
+            DateRecognizer(supported_language="he", context=["תאריך", "לידה", "הולדת", "נולד", "נולדה", "נולדו"]))
+        ner_signals.append(
+            EmailRecognizer(supported_language="he", context=["אימייל", "דואל", "email", "דואר אלקטרוני"]))
+        ner_signals.append(IpRecognizer(supported_language="he", context=["IP", "כתובת IP", "כתובת איי פי"]))
+        ner_signals.append(PhoneRecognizer(supported_language="he", context=["טלפון", "סלולרי", "פקס"]))
+
+        hebspacy_recognizer = self.init_hebspacy_recognizer()
+
+        ner_signals.append(hebspacy_recognizer)
+        # init Israeli id number recognizer
+        ner_signals.append(IsraeliIdNumberRecognizer())
+        # init general id recognizer
+        ner_signals.append(GeneralIdRecognizer())
+        # init dates in hebrew
+        # ner_signals.append(HebDateRecognizer())
+        # init dates with preposition
+        ner_signals.append(PrepositionDateRecognizer())
+        # init latin dates
+        ner_signals.append(HebLatinDateRecognizer())
+        # init Hebrew country recognizer
+        # ner_signals.append(LexiconBasedRecognizer("CountryRecognizer", "COUNTRY", COUNTRY_DICT.keys(),
+        #                                           allowed_prepositions=LOCATION_PREPOSITIONS))
+        # init Hebrew city recognizer
+        # ner_signals.append(LexiconBasedRecognizer("IsraeliCityRecognizer", "CITY",
+        #                                           set(BELOW_THRESHOLD_CITIES_LIST).union(
+        #                                               set(ABOVE_THRESHOLD_CITIES_LIST)).union(set(ABBREVIATIONS_LIST)),
+        #                                           allowed_prepositions=LOCATION_PREPOSITIONS))
+        # init disease recognizer
+        ner_signals.append(
+            LexiconBasedRecognizer("DiseaseRecognizer", "DISEASE", DISEASES, allowed_prepositions=DISEASE_PREPOSITIONS))
+        # init medication recognizer
+        ner_signals.append(
+            LexiconBasedRecognizer("MedicationRecognizer", "MEDICATION", MEDICATIONS,
+                                   allowed_prepositions=MEDICATION_PREPOSITIONS))
+        return ner_signals
+
+    def init_hebspacy_recognizer(self):
+        """
+        Adapt the SpacyRecognizer to fit the specific entities of HebSpacy.
+        """
+
+        hebspacy_entities = ["PERS", "LOC", "ORG", "TIME", "DATE", "MONEY", "PERCENT", "MISC__AFF", "MISC__ENT",
+                             "MISC_EVENT", "PER", "GPE", "FAC", "WOA", "EVE", "DUC", "ANG"]
+        hebspacy_label_groups = [
+            ({ent}, {ent}) for ent in hebspacy_entities
+        ]
+        hebspacy_recognizer = SpacyRecognizer(supported_language="he",
+                                              supported_entities=hebspacy_entities,
+                                              ner_strength=1.0,
+                                              check_label_groups=hebspacy_label_groups)
+        return hebspacy_recognizer
diff --git a/hebsafeharbor/identifier/signals/__init__.py b/hebsafeharbor/identifier/signals/__init__.py
new file mode 100644
index 0000000..2a2d32a
--- /dev/null
+++ b/hebsafeharbor/identifier/signals/__init__.py
@@ -0,0 +1,5 @@
+from .general_id_recognizer import GeneralIdRecognizer
+from .israeli_id_recognizer import IsraeliIdNumberRecognizer
+from .spacy_recognizer_with_confidence import SpacyRecognizerWithConfidence
+
+__all__ = ["GeneralIdRecognizer", "IsraeliIdNumberRecognizer", "SpacyRecognizerWithConfidence"]
diff --git a/hebsafeharbor/identifier/signals/general_id_recognizer.py b/hebsafeharbor/identifier/signals/general_id_recognizer.py
new file mode 100644
index 0000000..ad8e1f1
--- /dev/null
+++ b/hebsafeharbor/identifier/signals/general_id_recognizer.py
@@ -0,0 +1,23 @@
+from presidio_analyzer import PatternRecognizer, Pattern
+
+
+class GeneralIdRecognizer(PatternRecognizer):
+    """
+    A class which extends the PatternRecognizer (@Presidio) and responsible for the recognition of IDs.
+    """
+
+    PATTERNS = [
+        Pattern(
+            "five or more digits optionally separated by dash",
+            r"(\d[-]?){4,}\d",
+            0.6)
+    ]
+
+    SUPPORTED_ENTITY = "ID"
+
+    def __init__(self):
+        """
+        Initializes the GeneralIdRecognizer object
+        """
+        super().__init__(supported_entity=GeneralIdRecognizer.SUPPORTED_ENTITY, patterns=GeneralIdRecognizer.PATTERNS,
+                         name="GeneralIdRecognizer", supported_language="he")
diff --git a/hebsafeharbor/identifier/signals/heb_date_recognizer.py b/hebsafeharbor/identifier/signals/heb_date_recognizer.py
new file mode 100644
index 0000000..863720f
--- /dev/null
+++ b/hebsafeharbor/identifier/signals/heb_date_recognizer.py
@@ -0,0 +1,30 @@
+from typing import Optional
+
+from presidio_analyzer import PatternRecognizer, Pattern
+from hebsafeharbor.common.date_regex import HEB_DATE_REGEX
+
+class HebDateRecognizer(PatternRecognizer):
+    """
+    A class which extends the PatternRecognizer (@Presidio) and responsible for the recognition of Hebrew dates (א׳ בתשרי תש״ח)
+    """
+    PATTERNS = [
+        Pattern(
+            "Hebrew full date",  
+            HEB_DATE_REGEX,
+            0.6
+        ),        
+        
+    ]
+
+    SUPPORTED_ENTITY = "DATE"
+    
+    def __init__(self):
+        """
+        Initializes the IdRecognizer object
+        """
+        super().__init__(supported_entity=HebDateRecognizer.SUPPORTED_ENTITY,
+                         patterns=HebDateRecognizer.PATTERNS, name="HebDateRecognizer",                         
+                         supported_language="he"
+                         )
+
+    
\ No newline at end of file
diff --git a/hebsafeharbor/identifier/signals/heb_latin_date_recognizer.py b/hebsafeharbor/identifier/signals/heb_latin_date_recognizer.py
new file mode 100644
index 0000000..f533bd2
--- /dev/null
+++ b/hebsafeharbor/identifier/signals/heb_latin_date_recognizer.py
@@ -0,0 +1,33 @@
+from typing import Optional
+
+from presidio_analyzer import PatternRecognizer, Pattern
+from hebsafeharbor.common.date_regex import LATIN_DATE_REGEX
+
+
+
+class HebLatinDateRecognizer(PatternRecognizer):
+    """
+    A class which extends the PatternRecognizer (@Presidio) and recognizes dates that contain the name of the month
+    (ינואר 1999 9)
+    """    
+    
+    PATTERNS = [
+        Pattern(
+            "day month year",
+            LATIN_DATE_REGEX,
+            0.6
+        ),                
+    ]
+
+    SUPPORTED_ENTITY = "DATE"
+    
+    def __init__(self):
+        """
+        Initializes the IdRecognizer object
+        """
+        super().__init__(supported_entity=HebLatinDateRecognizer.SUPPORTED_ENTITY,
+                         patterns=HebLatinDateRecognizer.PATTERNS, name="HebLatinDateRecognizer",                         
+                         supported_language="he"
+                         )
+
+    
\ No newline at end of file
diff --git a/hebsafeharbor/identifier/signals/heb_preposition_date_recognizer.py b/hebsafeharbor/identifier/signals/heb_preposition_date_recognizer.py
new file mode 100644
index 0000000..7e9b5f5
--- /dev/null
+++ b/hebsafeharbor/identifier/signals/heb_preposition_date_recognizer.py
@@ -0,0 +1,32 @@
+from typing import Optional, List
+from presidio_analyzer.predefined_recognizers import DateRecognizer
+from presidio_analyzer import Pattern
+
+class PrepositionDateRecognizer(DateRecognizer):
+    """
+    A class which extends the DateRecognizer (@Presidio) and recognizes dates that contain a Hebrew preposition (ב9.9.1999)
+    """   
+    def __init__(
+        self,
+        patterns: Optional[List[Pattern]] = None,
+        context: Optional[List[str]] = None,
+        supported_language: str = "he",
+        supported_entity: str = "DATE",
+    ):
+        # take the default patterns from the DateRecognizer class and augment them with a preposition
+        patterns = self.PATTERNS
+        AUG_PATTERNS = []
+        for p in patterns:
+            pattern_dict = p.to_dict()
+            pattern = pattern_dict['regex']
+            new_regex = pattern[:pattern.index(r'\b')+2] + '(?:ה|ב|מ|מה?)' + pattern[pattern.index(r'\b')+2:]
+            pattern_dict['regex'] = new_regex
+            AUG_PATTERNS.append(Pattern.from_dict(pattern_dict))
+        
+        super().__init__(
+            supported_entity=supported_entity,
+            patterns=AUG_PATTERNS,
+            context=context,
+            supported_language=supported_language,
+        )
+
diff --git a/hebsafeharbor/identifier/signals/israeli_id_recognizer.py b/hebsafeharbor/identifier/signals/israeli_id_recognizer.py
new file mode 100644
index 0000000..63b3efa
--- /dev/null
+++ b/hebsafeharbor/identifier/signals/israeli_id_recognizer.py
@@ -0,0 +1,60 @@
+from typing import Optional
+
+from presidio_analyzer import PatternRecognizer, Pattern
+
+
+class IsraeliIdNumberRecognizer(PatternRecognizer):
+    """
+    A class which extends the PatternRecognizer (@Presidio) and responsible for the recognition of Israeli ID number
+    entity and its validation.
+    """
+
+    PATTERNS = [
+        Pattern(
+            "9 digits israeli id number",
+            r"[0-9]{9}",
+            0.6
+        ),
+        Pattern(
+            "8 digits israeli id number (without the leading zero)",
+            r"[0-9]{8}",
+            0.6
+        ),
+        Pattern(
+            "9 digits israeli id number with a dash before the check digit (the last digit)",
+            r"[0-9]{8}-[0-9]",
+            0.6
+        ),
+    ]
+
+    SUPPORTED_ENTITY = "ISRAELI_ID_NUMBER"
+
+    def __init__(self):
+        """
+        Initializes the IdRecognizer object
+        """
+        super().__init__(supported_entity=IsraeliIdNumberRecognizer.SUPPORTED_ENTITY,
+                         patterns=IsraeliIdNumberRecognizer.PATTERNS, name="IsraeliIdNumberRecognizer",
+                         supported_language="he")
+
+    def validate_result(self, pattern_text: str) -> Optional[bool]:
+        """
+        Validate the israeli id number recognized pattern by running checksum on a detected pattern (Luhn algorithm).
+
+        :param pattern_text: the text to validated. Only the part in text that was detected by the regex engine
+        :return: A bool indicating whether the validation was successful.
+        """
+        pattern_text = pattern_text.replace("-", "")
+        if len(pattern_text) == 8:
+            pattern_text = "0" + pattern_text
+        check_digit = int(pattern_text[-1])
+        id_digits_str = pattern_text[:-1]
+        values_sum = 0
+        for index, digit_char in enumerate(id_digits_str):
+            digit = int(digit_char)
+            if index % 2 == 1:
+                digit *= 2
+            if digit > 10:
+                digit = int(digit / 10) + (digit % 10)
+            values_sum += digit
+        return 10 - (values_sum % 10) == check_digit
diff --git a/hebsafeharbor/identifier/signals/lexicon_based_recognizer.py b/hebsafeharbor/identifier/signals/lexicon_based_recognizer.py
new file mode 100644
index 0000000..ce8ada3
--- /dev/null
+++ b/hebsafeharbor/identifier/signals/lexicon_based_recognizer.py
@@ -0,0 +1,61 @@
+from typing import List
+from presidio_analyzer import EntityRecognizer, RecognizerResult, AnalysisExplanation
+from presidio_analyzer.nlp_engine import NlpArtifacts
+
+from hebsafeharbor.common.terms_recognizer import TermsRecognizer
+
+
+class LexiconBasedRecognizer(EntityRecognizer):
+    """
+    A class which extends the EntityRecognizer (@Presidio) and recognize entities based on a lexicon
+    """
+
+    DEFAULT_CONFIDENCE_LEVEL = 0.7  # expected confidence level for this recognizer
+
+    def __init__(self, name: str, supported_entity: str, phrase_list: List[str], supported_language: str = "he",
+                 allowed_prepositions=[]):
+        """
+        Initializes Hebrew LexiconBasedRecognizer
+
+        :param name: recognizer's name
+        :param supported_entity: entity type to be associated with the entities recognized by the lexicon based
+        recognizer
+        :param phrase_list: lexicon's phrases
+        :param supported_language: the language that the recognizer supports. Hebrew is the default
+        :param allowed_prepositions: prepositions that allowed to be recognized as part of the entity (in addition to
+        the lexicon phrase itself). Empty list (which means prepositions are not allowed) is the default
+        """
+        super().__init__(name=name, supported_entities=[supported_entity], supported_language=supported_language)
+        self.terms_recognizer = TermsRecognizer(phrase_list)
+        self.allowed_prepositions = allowed_prepositions
+
+    def load(self) -> None:
+        """No loading is required."""
+        pass
+
+    def analyze(
+            self, text: str, entities: List[str], nlp_artifacts: NlpArtifacts
+    ) -> List[RecognizerResult]:
+        """
+        Recognize entities based on lexicon
+        :param text: text for recognition
+        :param entities: supported entities
+        :param nlp_artifacts: artifacts of the nlp engine
+        :return list of entities recognized based on the lexicon
+        """
+        results = []
+
+        terms_offsets = self.terms_recognizer(text, prefixes=self.allowed_prepositions)
+
+        # Iterate over the Automaton offsets and create Recognizer result for each of them
+        for start_offset, length in terms_offsets:
+            result = RecognizerResult(
+                entity_type=self.supported_entities[0],
+                start=start_offset,
+                end=start_offset + length,
+                score=self.DEFAULT_CONFIDENCE_LEVEL,
+                analysis_explanation=AnalysisExplanation(self.name, self.DEFAULT_CONFIDENCE_LEVEL)
+            )
+            results.append(result)
+
+        return results
diff --git a/hebsafeharbor/identifier/signals/spacy_recognizer_with_confidence.py b/hebsafeharbor/identifier/signals/spacy_recognizer_with_confidence.py
new file mode 100644
index 0000000..eebe4bc
--- /dev/null
+++ b/hebsafeharbor/identifier/signals/spacy_recognizer_with_confidence.py
@@ -0,0 +1,63 @@
+import logging
+from collections import Set
+from typing import Tuple
+
+from presidio_analyzer import RecognizerResult
+from presidio_analyzer.predefined_recognizers import SpacyRecognizer
+
+
+class SpacyRecognizerWithConfidence(SpacyRecognizer):
+    """
+    Wrapper on SpacyRecognizer which outputs confidence scores, if provided.
+    Confidence scores should be an extension of the spaCy Span object, e.g. ent._.confidence_score.
+    """
+
+    def analyze(self, text, entities, nlp_artifacts=None):
+        results = []
+        ner_entities = nlp_artifacts.entities
+
+        for entity in entities:
+            if entity not in self.supported_entities:
+                continue
+            for ent in ner_entities:
+                if not self.__check_label(entity, ent.label_, self.check_label_groups):
+                    continue
+                if not ent.has_extension("confidence_score"):
+                    raise ValueError(
+                        f"confidence score not available as a spaCy span extension "
+                        f"(ent._.confidence_score)"
+                    )
+
+                confidence_score = ent._.confidence_score
+                textual_explanation = self.DEFAULT_EXPLANATION.format(ent.label_)
+
+                explanation = self.build_spacy_explanation(
+                    original_score=confidence_score,
+                    explanation=textual_explanation,
+                )
+
+                spacy_result = RecognizerResult(
+                    entity,
+                    ent.start_char,
+                    ent.end_char,
+                    confidence_score,
+                    explanation,
+                )
+                results.append(spacy_result)
+
+        return results
+
+    @staticmethod
+    def __check_label(
+        entity: str, label: str, check_label_groups: Tuple[Set, Set]
+    ) -> bool:
+        """
+        Check whether the entity (from Presidio) matches the label (from spaCy).
+        :param entity: Entity name
+        :param label: Token label
+        :param check_label_groups: A tuple of sets of names for matching
+
+        """
+        return any(
+            [entity in egrp and label in lgrp for egrp, lgrp in check_label_groups]
+        )
diff --git a/hebsafeharbor/lexicons/__init__.py b/hebsafeharbor/lexicons/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/hebsafeharbor/lexicons/body_parts.py b/hebsafeharbor/lexicons/body_parts.py
new file mode 100644
index 0000000..cc6026e
--- /dev/null
+++ b/hebsafeharbor/lexicons/body_parts.py
@@ -0,0 +1,134 @@
+BODY_PARTS = [
+	"אבובית מקורבת",
+	"מערכת ההפרשה החיצונית",
+	"שיערות",
+	"אנדונויריום",
+	"בלוטת רוק",
+	"לולאת הנלי",
+	"המערכת הסימפטטית",
+	"סדן",
+	"בלוטות הרוק",
+	"להאה",
+	"ירכיים",
+	"הגומד",
+	"עילית העור",
+	"אנאלי",
+	"האיבר הספירלי של קורטי",
+	"האבובית הרחוקה",
+	"בלוטות הזיעה",
+	"ארכוף",
+	"מדולת האדרנל",
+	"האבובית הקרובה",
+	"מערכת העצבים הסימפטטית",
+	"ציצים",
+	"מסתם הלב",
+	"החך הרך",
+	"חיץ הנקבים",
+	"גזע המח",
+	"החומר הלבן",
+	"פרינאום",
+	"עצם העקב",
+	"המבוך הקרומי",
+	"תעלת אוסטכיוס",
+	"עצבים סימפטתיים",
+	"אגן ירכיים",
+	"מחושים",
+	"יותרת המוח",
+	"מערכת העצבים הפריפרית",
+	"נאדית הריאה",
+	"רקטום",
+	"בלוטת יותרת המח",
+	"אינומינטה",
+	"בלוטות הדמע",
+	"גלומרולוס",
+	"בלוטת היפופיזה",
+	"קרנית העין",
+	"עפעפים",
+	"טופרים",
+	"המעי הגס העולה",
+	"צפורן",
+	"תת רמה",
+	"עוקץ הדבורה",
+	"המבוך הגרמי",
+	"עצמימי השמע",
+	"ערקום",
+	"הגומץ הסגלגל",
+	"גאנגליון",
+	"קליפת המוח הקדם-מצחית הדורסו-לטראלית",
+	"המערכת הסימפתטית",
+	"ידיים",
+	"תת הרמה",
+	"לשד האדרנל",
+	"המסתם האאורטלי",
+	"מיתר גב",
+	"הנפרון הדיסטלי",
+	"קורפוס קלוסום",
+	"שק האשכים",
+	"עצם הערקום",
+	"שערות",
+	"פתולוגיה של ההיפותלמוס",
+	"לשד יותרת הכליה",
+	"מסתם אילאו-צקלי",
+	"פרצוף",
+	"האיבר הספירלי",
+	"היפוטלמוס",
+	"שערה",
+	"טורסו",
+	"הולכה חשמלית בלב",
+	"אגן הירכיים",
+	"טפר",
+	"בלוטת הדמע",
+	"חלל החזה",
+	"אצטבולום",
+	"תפוח אדם",
+	"מערכת העורקים",
+	"בלוטות אקסוקריניות",
+	"עפעפיים",
+	"ציפורניים",
+	"המערכת הפאראסימפתטית",
+	"חיך",
+	"החיך הקשה",
+	"לע",
+	"בלוטת יתרת המח",
+	"שסתום הלב",
+	"קורפוס קאלוזום",
+	"שסתומי הלב",
+	"גרוגרת",
+	"ליבת האדרנל",
+	"כפיס-המוח",
+	"תעלת השמע",
+	"יתרת המוח",
+	"קולון",
+	"תת-רמה",
+	"המעי הגס היורד",
+	"חבל טבור",
+	"שלד גפיים",
+	"פטיש",
+	"מסתם טריקוספידלי",
+	"טפרים",
+	"שד",
+	"עצם הטריז",
+	"שער בית השחי",
+	"קשתית העין",
+	"גומד",
+	"מערכת העצבים העצמונית",
+	"מערכת העצבים הפריפרלית",
+	"מערכת ההפרשה הפנימית",
+	"גלומרול",
+	"תעלת אוסטאכיוס",
+	"שיער בית שחי",
+	"העוקץ של הדבורה",
+	"האיבר הספירלי של השבלול",
+	"פטמת הטעם",
+	"בלוטת ההרת",
+	"בלוטת סקנה",
+	"היפופיזה",
+	"קופסית באומן",
+	"מערכת העצבים הפארא-סימפטטית",
+	"בלוטת ההיפופיזה",
+	"בלוטת יתרת המוח",
+	"מערך עורקי",
+	"תימוס",
+	"דרכי השתן",
+	"מפרק ממשחי",
+]
diff --git a/hebsafeharbor/lexicons/disease.py b/hebsafeharbor/lexicons/disease.py
new file mode 100644
index 0000000..e0b2380
--- /dev/null
+++ b/hebsafeharbor/lexicons/disease.py
@@ -0,0 +1,156 @@
+DISEASES = [
+	"תסמונת קוטר",
+	"אשליית השלילה",
+	"ענקיות",
+	"קסקסת העור",
+	"סינדרום קוטר",
+	"תת פעילות בלוטת האדרנל",
+	"לחץ דם גבוה",
+	"אוסטאומייליטיס",
+	"גבנון",
+	"דילוזיה ניהיליסטית",
+	"מעי דלקתי",
+	"ספסיס",
+	"תסמונת וולנברג",
+	"הפרעת דה פרסונליזציה",
+	"אלבינו",
+	"תת-חום",
+	"צמקת הכבד",
+	"סינדרום קוטאר",
+	"גרורות",
+	"צדעת",
+	"הפרעת נתק מהעצמי",
+	"היפוקסמיה",
+	"כלב משוגע",
+	"פס אנסרינוס בורסיטיס",
+	"ננסות",
+	"תת-חמצון",
+	"גסטריטיס",
+	"סכרת כלייתית",
+	"קרטוזיס",
+	"נזקי העישון",
+	"שחמת",
+	"עישון וסוכרת",
+	"טונסילולית",
+	"אריתמיה",
+	"אטלקטזיס",
+	"תסמונת קיסטר-האוזר",
+	"חסר חומצה פולית",
+	"לבקן",
+	"אפיקונדיליטיס",
+	"צלחה",
+	"דממת",
+	"כווית קור",
+	"הפרעת קריאה",
+	"גט לג",
+	"אי-סבילות ללקטוז",
+	"היפונתרמיה",
+	"תת חום",
+	"חוסר רצייה",
+	"אסטיגמטיזם",
+	"לקות התפתחותית",
+	"תת-סידניות",
+	"נפרופתיה",
+	"פגיעת קור",
+	"יבלת",
+	"נזקי עישון הטבק",
+	"תשעורת יתר",
+	"העדר רצייה",
+	"הפרעת ניתוק מהעצמי",
+	"חום נמוך",
+	"היפואדרנליזם",
+	"פרכוס לא אפילפטי",
+	"תסמונת רוקיטנסקי",
+	"תסמונת עש איגל",
+	"אנוריזמה",
+	"דלקת קרום המח",
+	"גינה גיז",
+	"היפרקיפוזיס",
+	"התקפי פאניקה",
+	"אמפיזמה",
+	"רבדומיוליזיס",
+	"אבן שקדים",
+	"ברונכיטיס",
+	"אלח דם חמור",
+	"תת-פעילות בלוטת יותרת הכליה",
+	"היעדר רצייה",
+	"עישון סיגריות",
+	"מנינגיטיס",
+	"התקף מוקדי",
+	"תת חמצון",
+	"דלקות בקיבה",
+	"היפר קיפוזיס",
+	"הכחשה עצמית",
+	"טניס אלבו",
+	"הפרעה טורדנית כפייתית ביחסים",
+	"הרטבה במיטה",
+	"התקף חלקי",
+	"רגל שטוחה",
+	"קטואצידוזיס סוכרתי",
+	"כאבי שרירים רב שגרוניים",
+	"מחלת המעי הדלקתי",
+	"המעי הרגיש",
+	"חטוטרת",
+	"חוסר פולאט",
+	"תסמונת קוטארד",
+	"טונסילוליט",
+	"גיגנטיזם",
+	"נפחת הריאות",
+	"אי סבילות ללקטוז",
+	"תסמונת הסטיילו-קרוטידית",
+	"תסמונת הסטיילו-היודית",
+	"תת-פעילות בלוטת האדרנל",
+	"ציקונגוניה",
+	"פיילת",
+	"קרטוסיס",
+	"ספחת",
+	"פציעה ריאתית",
+	"גיגאנטיזם",
+	"היפרפנילאלנינמיה",
+	"כויית קור",
+	"הפרעת דפרסונליזציה",
+	"הפרעת כתיבה",
+	"חסר של פולאט",
+	"אסטיגמטיות",
+	"הפרעות פסיכוגניות",
+	"גמד",
+	"המעי הרגיז",
+	"סינדרום טרנר",
+	"אפזיית ורניקה",
+	"הפרעות הולכה והפרעות קצב",
+	"חמצת קטוטית",
+	"טחור",
+	"סרטן המוח",
+	"קור תסמין למחלה",
+	"גיבנת",
+	"קפוט מדוזה",
+	"חסר פולאט",
+	"דיסלקציה",
+	"תאומי סיאם",
+	"קטואצידוזיס",
+	"פולימיאלגיה ראומטיקה",
+	"ורידים דליתיים",
+	"תת-סידן בדם",
+	"דיספלסיה",
+	"איחור התפתחותי",
+	"קשקשת העור",
+	"אמבליופיה",
+	"כף רגל שטוחה",
+	"סינדרום מחלקת תיירים",
+	"תמט ריאה",
+	"קיפוזיס",
+	"אשליה ניהיליסטית",
+	"תאומות סיאמיות",
+	"דלקת הסימפונות",
+	"פרפור לב",
+	"חוסר של פולאט",
+	"תסמונת הנשר",
+	"טורטיקוליס",
+	"התקף פאניקה",
+	"ענקות",
+	"מיזופוניה",
+	"טונסילוליתים",
+	"מעי רגיז",
+	"כשל בהפריה חוץ גופית",
+	"טונסילוליטים",
+]
diff --git a/hebsafeharbor/lexicons/hospital_lexicon.py b/hebsafeharbor/lexicons/hospital_lexicon.py
new file mode 100644
index 0000000..823b6f5
--- /dev/null
+++ b/hebsafeharbor/lexicons/hospital_lexicon.py
@@ -0,0 +1,164 @@
+HOSPITALS = [
+	"המרכז הרפואי כרמל",
+	"בית החולים אברבנאל",
+	"בית חולים פלימן",
+	"ביח קפלן",
+	"כפר שאול",
+	"המרכז הרפואי סורוקה",
+	"בית חולים איכילוב",
+	"אלין",
+	"בית החולים הלל יפה",
+	"בית חולים מעייני הישועה",
+	"בית החולים פטייה סלפטרייר",
+	"מרכז רפואי סורוקה",
+	"בית חולים לוינשטיין",
+	"בית החולים בנהריה",
+	"שלוותה",
+	"קפלן רחובות",
+	"בית חולים רמבם",
+	"המרכז הרפואי על שם שיבא",
+	"בית החולים של הקרמלין",
+	"המרכז לבריאות הנפש גהה",
+	"המרכז לבריאות הנפש - ירושלים",
+	"בית החולים העמק",
+	"בית החולים לגליל המערבי",
+	"בית החולים גהה",
+	"ביח רמבם",
+	"בית חולים וולפסון",
+	"המרכז הרפואי בני ציון",
+	"מרכז רפואי וולפסון",
+	"המרכז הרפואי מעיני הישועה",
+	"ביח לניאדו",
+	"בית חולים לגליל המערבי - נהריה",
+	"אסף הרופא",
+	"מרכז רופאי תל השומר",
+	"בית החולים אדמונד ולילי ספרא לילדים",
+	"בית חולים כרמל",
+	"עין כרם",
+	"הדסה עין-כרם",
+	"המרכז הרפואי רבין",
+	"המרכז הרפואי סוראסקי",
+	"בית החולים רמבם",
+	"עש ליידי דייויס",
+	"בית חולים הלל יפה",
+	"בית החולים כרמל",
+	"מרכז רפואי הלל יפה",
+	"בית החולים שיבא תל השומר",
+	"בית החולים קפלן",
+	"בית חולים גהה",
+	"מרכז רפואי רמבם",
+	"המרכז הרפואי שיבא",
+	"המרכז הירושלמי לבריאות הנפש",
+	"הדסה",
+	"לניאדו",
+	"מוריסטאן",
+	"בית החולים הקליני המרכזי",
+	"מרכז רפואי שערי צדק",
+	"המרכז הרפואי ברזילי",
+	"בית החולים בילינסון",
+	"בית חולים אסף הרופא",
+	"בית חולים העמק",
+	"בית החולים הרצוג",
+	"מרכז רפואי שמיר",
+	"בית החולים אסף הרופא",
+	"המרכז הרפואי תל אביב",
+	"בית החולים דנה לילדים",
+	"בית לוינשטיין",
+	"בית החולים פלימן",
+	"בילינסון",
+	"בית חולים משגב לדך",
+	"ביח שיבא",
+	"בית החולים הסקוטי",
+	"מרכז רפואי שיבא",
+	"בית החולים נהריה",
+	"ביסטרה",
+	"בית רבקה בית חולים",
+	"בית החולים שיבא בתל השומר",
+	"בית החולים שערי צדק",
+	"ביח העמק",
+	"בית החולים ספרא לילדים",
+	"שיפא",
+	"רמבם-הקריה הרפואית לבריאות האדם",
+	"בית החולים הסקוטי בנצרת",
+	"בית חולים עזרת נשים",
+	"המרכז היהודי הלאומי לרפואה ולמחקר דנבר",
+	"בית החולים למצורעים עזרת ישו",
+	"בניין אוגוסטה ויקטוריה",
+	"בית החולים יוספטל",
+	"אכסניית אוגוסטה ויקטוריה",
+	"המרכז הרפואי וולפסון",
+	"המרכז לבריאות הנפש ירושלים",
+	"בית חולים שיבא",
+	"ביהח שיפא",
+	"קרמליובקה",
+	"בית החולים פוריה",
+	"ביילינסון",
+	"בית החולים איכילוב",
+	"בית חולים קפלן",
+	"בית החולים תל השומר",
+	"בית חולים תל השומר",
+	"בית החולים לניאדו",
+	"תישרין",
+	"המרכז הרפואי רמבם",
+	"בית לווינשטיין",
+	"בית החולים לחולי נפש כפר שאול",
+	"עזרהבינוויקי בוויקינתונים",
+	"בית החולים אל מקאסד",
+	"המרכז הרפואי על שם אסף הרופא",
+	"בית החולים תל-השומר",
+	"סלפטרייר",
+	"בית החולים ברזילי",
+	"המרכז הרפואי תא",
+	"עזרת ישו",
+	"המרכז הרפואי תל-אביב",
+	"בית החולים סורוקה",
+	"מרכז רפואי מעייני הישועה",
+	"מרכז רפואי אשדוד",
+	"בית החולים למצורעים הנסן",
+	"המרכז הרפואי אסף הרופא",
+	"הדסה עין כרם",
+	"המרכז הרפואי העמק",
+	"המרכז לסרטן עש מד אנדרסון באוניברסיטת טקסס",
+	"בית חולים נהריה",
+	"המרכז הרפואי על שם חיים שיבא תל השומר",
+	"בית חולים למצורעים עש האנסן",
+	"בית חולים ברזילי",
+	"בית חולים ביקור חולים",
+	"מרכז רפואי אסף הרופא",
+	"בית החולים הדסה עין-כרם",
+	"בית החולים עש ברוך פדה",
+	"בית ספר שחף",
+	"בית החולים אל-מוקאסד",
+	"מתחם המוריסטאן",
+	"בית חולים סנט וינסנט",
+	"המוריסטאן",
+	"ביח כרמל",
+	"בית החולים עזרת נשים",
+	"אברבנאל בית חולים",
+	"המרכז הרפואי עש ברזילי",
+	"מרכז רפואי יוספטל",
+	"בית החולים וולפסון",
+	"המרכז הרפואי מאיר",
+	"בית החולים מעייני הישועה",
+	"המרכז הרפואי איכילוב",
+	"בית החולים למחלות עיניים סנט גון בירושלים",
+	"בית חולים שערי צדק",
+	"קפלן",
+	"בית חולים הקריה",
+	"עזרת נשים",
+	"בית חולים סורוקה",
+	"המרכז הרפואי יצחק שמיר",
+	"הקריה הרפואית רמבם",
+	"בית החולים הממשלתי בחיפה",
+	"תשרין",
+	"בית החולים למצורעים עש האנסן",
+	"כנסיית אוגוסטה ויקטוריה",
+	"אוגוסטה ויקטוריה",
+	"בית חולים יוספטל",
+	"המוריסתאן",
+	"מרכז רפואי תל אביב",
+	"בית החולים שיבא",
+	"המרכז הרפואי קפלן",
+	"המרכז הרפואי מעייני הישועה",
+	"בית חולים מאיר",
+]
diff --git a/hebsafeharbor/lexicons/medications.py b/hebsafeharbor/lexicons/medications.py
new file mode 100644
index 0000000..744f7dc
--- /dev/null
+++ b/hebsafeharbor/lexicons/medications.py
@@ -0,0 +1,227 @@
+MEDICATIONS = [
+	"גז צחוק",
+	"אנטיטריפסין",
+	"מטאמיזול",
+	"סודיום ניטריט",
+	"פרוקור",
+	"חומצה פירובית",
+	"קליום פרמנגנט",
+	"ניטרופורנטואין",
+	"רביף",
+	"טריל",
+	"סודה לשתיה",
+	"דילנטין",
+	"קסנאקס",
+	"קומדין",
+	"טרבינפין",
+	"מתאמפטמין",
+	"מטאמפטמין",
+	"לופרקס",
+	"סרוקואל",
+	"למיסיל",
+	"פרמנגנט האשלגן",
+	"למודקס",
+	"דקסטרואמפטמין",
+	"הרספטין",
+	"פרדניסון",
+	"דיאקומיט",
+	"ריבופלבין",
+	"דרמסיל",
+	"אספרטט",
+	"טירוזינמיה",
+	"קפרה",
+	"ביאטה",
+	"וי-טלגין",
+	"פירובאט",
+	"טבענט",
+	"בייטה",
+	"טגרטול",
+	"ריבופלאבין",
+	"תרוצידין",
+	"סורמין",
+	"ציסטין",
+	"חומצה פירווית",
+	"אלפראזולם",
+	"אדנוזין תלת-פוספט",
+	"פרותיאזין",
+	"קסאנאקס",
+	"פירווט",
+	"זאלדיאר",
+	"פנלגין",
+	"רמונאבאנט",
+	"רמונבאנט",
+	"ציפרלקס",
+	"אכומפליה",
+	"כלורפרומזין",
+	"אופטלגין",
+	"תריאונין",
+	"אטפ",
+	"באיטה",
+	"אספרטיים",
+	"מיוזיים",
+	"בייאטה",
+	"לופרמיד",
+	"רפאמיצין",
+	"סירולימוס",
+	"ריבופלאווין",
+	"למיזיל",
+	"הפרין",
+	"טרסיבה",
+	"גלותטיון",
+	"סינטומיצין",
+	"נתרן ביקרבונט",
+	"טמיפלו",
+	"מסלאמין",
+	"נרדיל",
+	"מורפין דיאצטט",
+	"מורפיום",
+	"חמצן דו חנקני",
+	"דופאמין",
+	"מקסיבון",
+	"אמוקסי",
+	"אקטמרה",
+	"פחמן פעיל",
+	"חומצה אסקורבית",
+	"פירוואט",
+	"גלוקופאג",
+	"ואלין",
+	"תיאמין",
+	"קסנקס",
+	"סימבלטה",
+	"בטרפן",
+	"מת קריסטלי",
+	"רימונאבנט",
+	"מלתונין",
+	"קריאטין",
+	"לויצין",
+	"אלפרזולם",
+	"אדנוזין טרי פוספט",
+	"אתאנול",
+	"ציכלומד",
+	"תירוזין",
+	"וליום",
+	"אלפרזולאם",
+	"סטופיט",
+	"מתאמפתמין",
+	"דיאזפם",
+	"דיאמורפין",
+	"שאבו",
+	"למיקטל",
+	"רימונבנט",
+	"בווציזומאב",
+	"אימורן",
+	"לאמיזיל",
+	"אשלגן על-מנגנאט",
+	"טטראהידרוקנאבינול",
+	"טרצבה",
+	"אלרין",
+	"ביטה",
+	"גלוטטיון",
+	"אשלגן כלוריד",
+	"הורמון אדרנוקורטיקוטרופיק",
+	"תאין",
+	"יאבא",
+	"רמונבנט",
+	"ויוקס",
+	"אפינפרין",
+	"אדנוזין טריפוספט",
+	"ואליום",
+	"דרמאזיל",
+	"ניטריט הנתרן",
+	"קוואטיאפין",
+	"מטאמפתמין",
+	"נוראפינפרין",
+	"זימולטי",
+	"רימונאבאנט",
+	"אוונקס",
+	"אזופי",
+	"אמיודקור",
+	"קריסטל מת",
+	"תראולון",
+	"קופקסון",
+	"טרמדקס",
+	"דיאצטילמורפין",
+	"כלורומפניקול",
+	"סודיום ביקרבונט",
+	"אצטיל כולין",
+	"רמונאבנט",
+	"הירואין",
+	"הורמון נוגד השתנה",
+	"מת",
+	"זאנקס",
+	"פרמנגנט האשלג",
+	"לופי-קאר",
+	"הורמון שחרור תירוטרופין",
+	"פרמנגנאט האשלג",
+	"סם הרגעה",
+	"ניאצין",
+	"אביגן",
+	"מסלאזין",
+	"רדוקטיל",
+	"אצתיל כולין",
+	"פוסלן",
+	"ניקוטינאמיד",
+	"אימודיום",
+	"אספרטאט",
+	"פמברוליזומב",
+	"סרוקוול",
+	"אקומפליה",
+	"כינידוראן",
+	"נייטרוס",
+	"מנטול",
+	"טריפטופאן",
+	"אדנוזין תלת פוספט",
+	"חומצה סאליצילית",
+	"למוגין",
+	"רימונה באנט",
+	"ספירט",
+	"דקסמטזון",
+	"מתילאמפטמין",
+	"גילניה",
+	"טראונין",
+	"זייבאן",
+	"גמא-הידרוקסי-בוטיראט",
+	"ארלוטיניב",
+	"אורפנאדרין",
+	"אנטיטריפסין אלפא-1",
+	"הידרוקורטיזון",
+	"רימונה בנט",
+	"היסטדין",
+	"תאלידומיד",
+	"לוותרים",
+	"קסאנקס",
+	"זייבן",
+	"טרמל",
+	"מתמפתמין",
+	"נתרן ניטריטי",
+	"ואזופרסין",
+	"טריאונין",
+	"זאנאקס",
+	"אדנוזין תלת-זרחתי",
+	"אימוראן",
+	"וזופרסין",
+	"זובירס",
+	"זנאקס",
+	"זנקס",
+	"קפסין",
+	"לופריד",
+	"קסרלטו",
+	"אסיול",
+	"חנקן חד-חמצני",
+	"סם האונס",
+	"ריספרדל",
+	"אסציטאלופראם",
+	"רקמיד",
+	"פריזיום",
+	"כינידין",
+	"לקספרו",
+	"ויטמין סי",
+	"קראק",
+	"קוק פרסי",
+	"קטיאפין",
+	"כלוראמפניקול",
+	"אפנוטין",
+	"פרמנגנאט האשלגן",
+	"מוקסיפן",
+	"חומצה קרבולית",
+]
diff --git a/hebsafeharbor/manager.py b/hebsafeharbor/manager.py
new file mode 100644
index 0000000..85df524
--- /dev/null
+++ b/hebsafeharbor/manager.py
@@ -0,0 +1,71 @@
+from typing import Dict, List
+
+from hebsafeharbor import Doc
+from hebsafeharbor.anonymizer.phi_anonymizer import PhiAnonymizer
+from hebsafeharbor.identifier.phi_identifier import PhiIdentifier
+
+class HebSafeHarbor:
+    """
+    The manager of the application. When it called, given a Hebrew text, it executes the identification and
+    anonymization process and return an anonymized text.
+    """
+
+    def __init__(self):
+        """
+        Initializes HebSafeHarbor
+        """
+        self.identifier = PhiIdentifier()
+        self.anonymizer = PhiAnonymizer()
+
+    def __call__(self, doc_list: List[Dict[str, str]]) -> List[Doc]:
+        """
+        The main method, executes the PHI reduction process on the given text
+        :param doc_list: List of dictionary where each dict represents a document.
+                        Each dictionary should consist of "id" and "text" columns
+        :return: anonymized text
+        """
+        docs = [Doc(doc_dict) for doc_dict in doc_list]
+        docs = self.identify(docs)
+        docs = self.anonymize(docs)
+        return docs
+
+    def identify(self, docs: List[Doc]) -> List[Doc]:
+        """
+        This method identifies the PHI entities in the input text
+        :param docs: a list of Doc objects which contains the input text for anonymization
+        :return: a list of the updated Doc objects that contains the recognized PHI entities
+        """
+        return [self.identifier(doc) for doc in docs]
+
+    def anonymize(self, docs: List[Doc]) -> List[Doc]:
+        """
+        This method anonymizes the recognized PHI entities using different techniques
+        :param doc: a list of Doc objects which contains the consolidated recognized PHI entities
+        :return: a list of the updated Doc objects that contains the anonymized text
+        """
+        return [self.anonymizer(doc) for doc in docs]
+
+    @staticmethod
+    def create_result(doc: Doc) -> Dict[str, str]:
+        """
+        this function will get a document and create a result map.
+        """
+
+        items = []
+        for item in doc.anonymized_text.items:
+            item_result = {
+                "startPosition": item.start,
+                "endPosition": item.end,
+                "entityType": item.entity_type,
+                "text": doc.text[item.start:item.end],
+                "mask": item.text,
+                "operator": item.operator
+            }
+            items.append(item_result)
+
+        result: Dict = {
+            "id": doc.id,
+            "text": doc.anonymized_text.text,
+            "items": items
+        }
+        return result
diff --git a/hebsafeharbor/version.py b/hebsafeharbor/version.py
new file mode 100644
index 0000000..3504ab2
--- /dev/null
+++ b/hebsafeharbor/version.py
@@ -0,0 +1,5 @@
+_MAJOR = "0"
+_MINOR = "0"
+_REVISION = "3"
+
+VERSION = f"{_MAJOR}.{_MINOR}.{_REVISION}"
diff --git a/hsh_service.py b/hsh_service.py
new file mode 100644
index 0000000..5ec0178
--- /dev/null
+++ b/hsh_service.py
@@ -0,0 +1,63 @@
+import threading
+from enum import Enum
+from typing import Dict, List
+
+from hebsafeharbor import HebSafeHarbor
+
+
+class ServiceStatus(Enum):
+    UNINITIALIZED = 1
+    LOADING = 2
+    READY = 3
+    ERROR = 4
+
+
+class HebSafeHarborService:
+    def __init__(self):
+        self.hch: HebSafeHarbor = None
+        self.status = ServiceStatus.UNINITIALIZED
+
+    def _initialize(self):
+        self.status = ServiceStatus.LOADING
+        try:
+            hch = HebSafeHarbor()
+
+            doc = hch(
+                [{"id": "id", "text": "גדעון לבנה הגיע ב16.1.2022 לבית החולים שערי צדק עם תלונות על כאבים בחזה"}])
+
+            self.status = ServiceStatus.READY
+            self.hch = hch
+            print("Hebrew Safe Harbor Service is up and ready to serve")
+
+        except Exception as e:
+            self.status = ServiceStatus.ERROR
+            raise e
+
+    def load_async(self):
+        if self.status == ServiceStatus.UNINITIALIZED:
+            load_model_thread = threading.Thread(
+                target=hsh_service._initialize)
+            load_model_thread.start()
+
+    def ready(self):
+        if self.status == ServiceStatus.READY:
+            readiness, status_code = "ready", 200
+        elif self.status in [ServiceStatus.UNINITIALIZED, ServiceStatus.LOADING]:
+            readiness, status_code = "unready", 503
+        else:  # self.status == ServiceStatus.ERROR:
+            readiness, status_code = "unready", 500
+        return {
+                   "service": "Hebrew Safe Harbor",
+                   "status": readiness
+               }, status_code
+
+    def query(self, docs: List[Dict[str, str]]):
+        # executing the prediction
+        try:
+            output_docs = self.hch(docs)
+            return output_docs, 200
+        except Exception as e:
+            return f"Bad response: {e}", 400
+
+
+hsh_service = HebSafeHarborService()
diff --git a/repatriation-dict-utf8bom.csv b/repatriation-dict-utf8bom.csv
deleted file mode 100644
index 895f793..0000000
--- a/repatriation-dict-utf8bom.csv
+++ /dev/null
@@ -1,357 +0,0 @@
-﻿country,deid_country
-".קונגו, רפ. דמוק","אפריקה"
-"144 לא רשום","לא ידוע"
-"180 לא רשום","לא ידוע"
-"280 לא רשום","לא ידוע"
-"290 לא רשום","לא ידוע"
-"300 לא רשום","לא ידוע"
-"690 לא רשום","לא ידוע"
-"80 לא רשום","לא ידוע"
-"852 לא רשום","לא ידוע"
-"880 לא רשום","לא ידוע"
-"NULL","לא ידוע"
-"אבחזיסטאן","אירופה"
-"אוגנדה","אפריקה"
-"אוזבקיסטן","ברית המועצות"
-"אוסטריה","אירופה"
-"אוסטרליה","אוסטרליה ואוקיאניה"
-"אוקראינה","ברית המועצות"
-"אורוגוואי","אמריקה דרומית ומרכזית"
-"אזוואד","אפריקה"
-"אזור תעלת פנמה","אמריקה דרומית ומרכזית"
-"אזרביג'אן","ברית המועצות"
-"אזרבידג'אן","ברית המועצות"
-"אזרבייג'ן","ברית המועצות"
-"איחוד האמירויות הערב","אסיה"
-"איחוד האמירויות הערביות","אסיה"
-"איטליה","אירופה"
-"איי בהאמה","אמריקה דרומית ומרכזית"
-"איי גילברט ואליס","אוסטרליה ואוקיאניה"
-"איי הבתולה","אמריקה דרומית ומרכזית"
-"איי ווליס ופוטונה","אוסטרליה ואוקיאניה"
-"איי טורקס וקייקוס","אמריקה דרומית ומרכזית"
-"איי מרשל","אוסטרליה ואוקיאניה"
-"איי נורפולק","אוסטרליה ואוקיאניה"
-"איי סיישל","אפריקה"
-"איי פאלקלנד","אמריקה דרומית ומרכזית"
-"איי פארו","אירופה"
-"איי פיגי","אוסטרליה ואוקיאניה"
-"איי קומורו","אפריקה"
-"איי קוק","אוסטרליה ואוקיאניה"
-"איי קיימן","אמריקה דרומית ומרכזית"
-"איי קייפ וורדה","אפריקה"
-"איי שלמה","אוסטרליה ואוקיאניה"
-"איי שלמה הבריטיים","אוסטרליה ואוקיאניה"
-"אינדונזיה","אסיה"
-"אינדונסיה פיליפינים","אסיה"
-"איסלנד","אירופה"
-"איראן","אסיה"
-"אירופה לא ידוע","אירופה"
-"אירלנד","אירופה"
-"אל סלבדור","אמריקה דרומית ומרכזית"
-"אל סלוודור","אמריקה דרומית ומרכזית"
-"אלבניה","אירופה"
-"אלג'יריה","אפריקה"
-"אלגיריה","אפריקה"
-"אמירויות המפרץ הפרסי","אסיה"
-"אמריקה ואוקיניה לא ידוע","לא ידוע"
-"אמריקה לא ידוע","אמריקה דרומית ומרכזית"
-"אנגולה","אפריקה"
-"אנדורה","אירופה"
-"אנטיגואה וברבודה","אמריקה דרומית ומרכזית"
-"אסוואטיני","אפריקה"
-"אסטוניה","ברית המועצות"
-"אסיה ארץ לא ידועה","אסיה"
-"אסיה לא ידוע","אסיה"
-"אפגניסטן","אסיה"
-"אפריקה לא ידוע","אפריקה"
-"אקוודור","אמריקה דרומית ומרכזית"
-"ארגנטינה","אמריקה דרומית ומרכזית"
-"אריתריאה","אפריקה"
-"ארמניה","ברית המועצות"
-"ארצות הברית","אמריקה צפונית"
-"אתיופיה","אתיופיה"
-"בדרך לארץ","לא ידוע"
-"בהוטן","אסיה"
-"בהמס","אמריקה דרומית ומרכזית"
-"בוטסואנה","אפריקה"
-"בולגריה","אירופה"
-"בוליביה","אמריקה דרומית ומרכזית"
-"בוסניה","אירופה"
-"בוסניה והרצגובינה","אירופה"
-"בורונדי","אפריקה"
-"בורמה תאילנד מאלזיה סנגפור קמבודיה ולאוס","אסיה"
-"בורקינה פאסו","אפריקה"
-"בחריין","אסיה"
-"בילורוסיה","ברית המועצות"
-"בלארוס","ברית המועצות"
-"בלב ים","לא ידוע"
-"בלגיה","אירופה"
-"בליז","אמריקה דרומית ומרכזית"
-"בנגלדש","אסיה"
-"בנין","אפריקה"
-"בנין (דהומי)","אפריקה"
-"ברבדוס","אמריקה דרומית ומרכזית"
-"ברוניי","אסיה"
-"ברזיל","אמריקה דרומית ומרכזית"
-"בריטניה","אירופה"
-"ברית המועצות","אירופה"
-"ברמודה","אמריקה דרומית ומרכזית"
-"ג'יבוטי","אפריקה"
-"ג'מייקה","אמריקה דרומית ומרכזית"
-"גאורגיה","ברית המועצות"
-"גאנה","אפריקה"
-"גבון","אפריקה"
-"גואדאלופ","אמריקה דרומית ומרכזית"
-"גואטמלה","אמריקה דרומית ומרכזית"
-"גואם","אסיה"
-"גווטאמלה","אמריקה דרומית ומרכזית"
-"גיאנה","אמריקה דרומית ומרכזית"
-"גיאנה הצרפתית","אמריקה דרומית ומרכזית"
-"גיברלטר","אירופה"
-"גינאה","אפריקה"
-"גינאה ביסאו","אפריקה"
-"גינאה החדשה","אוסטרליה ואוקיאניה"
-"גינאה המשוונית","אפריקה"
-"גמביה","אפריקה"
-"גרוזיה","ברית המועצות"
-"גרמניה","אירופה"
-"גרמניה המזרחית","אירופה"
-"גרמניה המערבית","אירופה"
-"גרנדה","אמריקה דרומית ומרכזית"
-"דומיניקה","אמריקה דרומית ומרכזית"
-"דנמרק","אירופה"
-"דנמרק איסלנד","אירופה"
-"דרום אפריקה","אפריקה"
-"דרום סודאן","אפריקה"
-"האיטי","אמריקה דרומית ומרכזית"
-"האיים ההברדיים","אירופה"
-"האיים ההברידיים","אירופה"
-"האיים המלדיביים","אסיה"
-"האנטיליים ההולנדים","אמריקה דרומית ומרכזית"
-"האנטילים ההולנדים","אמריקה דרומית ומרכזית"
-"הגדה המערבית","אסיה"
-"הודו","אסיה"
-"הודו פקיסטן וציילון","אסיה"
-"הוואי","אמריקה צפונית"
-"הולנד","אירופה"
-"הונג קונג","אסיה"
-"הונגריה","אירופה"
-"הונדורס","אמריקה דרומית ומרכזית"
-"הטריטוריה הבריטית באוקיינוס ההודי","אסיה"
-"הממלכה המאוחדת","אירופה"
-"הממלכה המאוחדת (אנגליה)","אירופה"
-"הפיליפינים","אסיה"
-"הרפ הדומיניקנית","אמריקה דרומית ומרכזית"
-"הרפובליקה הדומיניקנית","אמריקה דרומית ומרכזית"
-"הרפובליקה הדמוקרטית של קונגו","אפריקה"
-"הרפובליקה הטורקית של צפון קפריסין","אירופה"
-"הרפובליקה המרכז-אפריקאית","אפריקה"
-"הרפובליקה הסינית","אסיה"
-"הרפובליקה העממית של סין","אסיה"
-"הרפובליקה הקטלאנית","אירופה"
-"הרפובליקה של קונגו","אפריקה"
-"ויטנאם הדרומית","אסיה"
-"וייטנאם","אסיה"
-"ונואטו","אוסטרליה ואוקיאניה"
-"ונצואלה","אמריקה דרומית ומרכזית"
-"ותיקן","אירופה"
-"זימבאבווה","אפריקה"
-"זימבבואה","אפריקה"
-"זמביה","אפריקה"
-"חבש אריתריאה סומאלי","אפריקה"
-"חוף השנהב","אפריקה"
-"טאיוואן","אסיה"
-"טג'יקיסטן","ברית המועצות"
-"טובאלו","אוסטרליה ואוקיאניה"
-"טוגו","אפריקה"
-"טונגה","אוסטרליה ואוקיאניה"
-"טוניס","אפריקה"
-"טורקיה","אירופה"
-"טורקמניסטן","ברית המועצות"
-"טימור הפורטוגלית","אסיה"
-"טנגר","אפריקה"
-"טנזניה","אפריקה"
-"טרינידד וטובגו","אמריקה דרומית ומרכזית"
-"י פ ן","אסיה"
-"יבשה וארץ לא ידועה","לא ידוע"
-"יהודה ושומרון","אסיה"
-"יוגוסלביה","אירופה"
-"יוון","אירופה"
-"יפן","אסיה"
-"ירדן","אסיה"
-"ישראל","ישראל"
-"כווית","אסיה"
-"כורדיסטן","אסיה"
-"כף ורדה","אפריקה"
-"לא ידוע","לא ידוע"
-"לא מוגדר","לא ידוע"
-"לא רשום","לא ידוע"
-"לאוס","אסיה"
-"לבנון","אסיה"
-"לוב","אפריקה"
-"לוקסמבורג","אירופה"
-"לטביה","ברית המועצות"
-"ליבריה","אפריקה"
-"ליטא","ברית המועצות"
-"ליכטנשטיין","אירופה"
-"לסוטו","אפריקה"
-"מאוריטניה","אפריקה"
-"מאוריציוס","אפריקה"
-"מאלי","אפריקה"
-"מדגסקר","אפריקה"
-"מדינות חצי האי ערב","אסיה"
-"מוזמביק","אפריקה"
-"מולדובה","ברית המועצות"
-"מונגוליה","אסיה"
-"מונטנגרו","אירופה"
-"מונסרת","אמריקה דרומית ומרכזית"
-"מונקו","אירופה"
-"מזרח טימור","אסיה"
-"מיאנמר","אסיה"
-"מיאנמר (בורמה)","אסיה"
-"מיאנמר-בורמה","אסיה"
-"מיקרונזיה","אוסטרליה ואוקיאניה"
-"מלאווי","אפריקה"
-"מלדיבים","אסיה"
-"מלזיה","אסיה"
-"מלטה","אירופה"
-"מצרים","אפריקה"
-"מצרים סודאן","אפריקה"
-"מקאו","אסיה"
-"מקדוניה","אירופה"
-"מקדוניה הצפונית","אירופה"
-"מקסיקו","אמריקה צפונית"
-"מרוקו","אפריקה"
-"מרוקו הספרדית","אפריקה"
-"מרוקו טנג'יר מרוקו הצרפתית","אפריקה"
-"מרטיניק","אמריקה דרומית ומרכזית"
-"מרכז אמריקה","אמריקה דרומית ומרכזית"
-"נאורו","אוסטרליה ואוקיאניה"
-"נורבגיה","אירופה"
-"נורווגיה","אירופה"
-"ניגר","אפריקה"
-"ניגריה","אפריקה"
-"ניו זילנד","אוסטרליה ואוקיאניה"
-"ניואה","אוסטרליה ואוקיאניה"
-"ניז'ר","אפריקה"
-"ניקרגואה","אמריקה דרומית ומרכזית"
-"נמיביה","אפריקה"
-"נפאל","אסיה"
-"סאו טומה ופרינסיפה","אפריקה"
-"סודאן","אפריקה"
-"סווזילנד","אפריקה"
-"סומליה","אפריקה"
-"סוריה","אסיה"
-"סורינאם","אמריקה דרומית ומרכזית"
-"סט הלנה","אפריקה"
-"סט וינסנט","אמריקה דרומית ומרכזית"
-"סט. לוציה","אמריקה דרומית ומרכזית"
-"סט. קיטס ונביס","אמריקה דרומית ומרכזית"
-"סיירה לאון","אפריקה"
-"סיירה ליאונה","אפריקה"
-"סיישל","אפריקה"
-"סין","אסיה"
-"סין הונג קונג","אסיה"
-"סינגפור","אסיה"
-"סיקים","אסיה"
-"סלובניה","אירופה"
-"סלובקיה","אירופה"
-"סמואה","אוסטרליה ואוקיאניה"
-"סמואה האמריקנית","אוסטרליה ואוקיאניה"
-"סמואה המערבית","אוסטרליה ואוקיאניה"
-"סן מרינו","אירופה"
-"סנגל","אפריקה"
-"סנט וינסנט והגרנדינים","אמריקה דרומית ומרכזית"
-"סנט לוסיה","אמריקה דרומית ומרכזית"
-"סנט קיטס ונוויס","אמריקה דרומית ומרכזית"
-"ספרד","אירופה"
-"ספרד גיברלטר אנדורה","אירופה"
-"סרביה","אירופה"
-"סרי לנקה","אסיה"
-"סרי לנקה (צילון)","אסיה"
-"סרי לנקה-צילון","אסיה"
-"עדן","אסיה"
-"עומאן","אסיה"
-"עיראק","אסיה"
-"ערב הסעודית","אסיה"
-"פאפואה","אוסטרליה ואוקיאניה"
-"פולין","אירופה"
-"פולינזיה הצרפתית","אוסטרליה ואוקיאניה"
-"פורטו ריקו","אמריקה דרומית ומרכזית"
-"פורטו-ריקו","אמריקה דרומית ומרכזית"
-"פורטוגל","אירופה"
-"פיג'י","אוסטרליה ואוקיאניה"
-"פינלנד","אירופה"
-"פלאו","אוסטרליה ואוקיאניה"
-"פנמה","אמריקה דרומית ומרכזית"
-"פפואה גינאה החדשה","אוסטרליה ואוקיאניה"
-"פקיסטן","אסיה"
-"פרגוואי","אמריקה דרומית ומרכזית"
-"פרו","אמריקה דרומית ומרכזית"
-"צ'אד","אפריקה"
-"צ'ילה","אמריקה דרומית ומרכזית"
-"צ'כוסלובקיה","אירופה"
-"צ'כיה","אירופה"
-"צ'צ'ניה","אסיה"
-"צכוסלובקיה","אירופה"
-"צ'כוסלובקיה","אירופה"
-"צרפת","אירופה"
-"צרפת מונקו","אירופה"
-"קובה","אמריקה דרומית ומרכזית"
-"קולומביה","אמריקה דרומית ומרכזית"
-"קומורו","אפריקה"
-"קונגו","אפריקה"
-"קונגו, רפ. דמוק.","אפריקה"
-"קוסובו","אירופה"
-"קוסטה ריקה","אמריקה דרומית ומרכזית"
-"קוריאה","אסיה"
-"קוריאה הדרומית","אסיה"
-"קוריאה הצפונית","אסיה"
-"קזחסטן","ברית המועצות"
-"קטר","אסיה"
-"קירג'זסטאן","ברית המועצות"
-"קירגיזסטן","ברית המועצות"
-"קירגיסטאן","ברית המועצות"
-"קיריבטי","אוסטרליה ואוקיאניה"
-"קלדוניה החדשה","אוסטרליה ואוקיאניה"
-"קמבודיה","אסיה"
-"קמרון","אפריקה"
-"קנדה","אמריקה צפונית"
-"קניה","אפריקה"
-"קפריסין","אירופה"
-"קרואטיה","אירופה"
-"קריית הוותיקן","אירופה"
-"ראוניון","אפריקה"
-"רואנדה","אפריקה"
-"רודזיה","אפריקה"
-"רומניה","אירופה"
-"רוסיה","ברית המועצות"
-"רמת הגולן","אסיה"
-"רפ מרכז אפריקנית","אפריקה"
-"רפובליקת עבר הקווקז","ברית המועצות"
-"רצועת עזה וסיני","אסיה"
-"שאר ארצות אוקיאניה","אוסטרליה ואוקיאניה"
-"שאר ארצות אירופה","אירופה"
-"שאר ארצות אמריקה הדרומית","אמריקה דרומית ומרכזית"
-"שאר ארצות אמריקה המרכזית","אמריקה דרומית ומרכזית"
-"שאר ארצות אסיה","אסיה"
-"שאר ארצות אפריקה","אפריקה"
-"שבדיה","אירופה"
-"שוודיה","אירופה"
-"שווייץ","אירופה"
-"שוויץ ליכטנשטיין","אירופה"
-"תאילנד","אסיה"
-"תגיקיסטאן","ברית המועצות"
-"תוניס","אפריקה"
-"תוניסיה","אפריקה"
-"תורכיה","אסיה"
-"תורכמניסטאן","ברית המועצות"
-"תימן","אסיה"
-"תימן הדמוקרטית","אסיה"
-"מכסיקו","אמריקה צפונית"
-"אקואדור","אמריקה דרומית ומרכזית"
-"בראזיל","אמריקה דרומית ומרכזית"
-"אירן","אסיה"
-"חבש אריתריאה סומאלי","חבש אריתריאה סומאלי"
-"ברית המועצות","ברית המועצות"
\ No newline at end of file
diff --git a/requirements-dev.txt b/requirements-dev.txt
new file mode 100644
index 0000000..55b033e
--- /dev/null
+++ b/requirements-dev.txt
@@ -0,0 +1,1 @@
+pytest
\ No newline at end of file
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000..764dc78
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,4 @@
+presidio-analyzer
+presidio-anonymizer
+pyahocorasick==1.4.2
+hebspacy
diff --git a/requirements_server.txt b/requirements_server.txt
new file mode 100644
index 0000000..a4579b3
--- /dev/null
+++ b/requirements_server.txt
@@ -0,0 +1,2 @@
+fastapi[all]
+https://github.com/8400TheHealthNetwork/HebSpacy/releases/download/he_ner_news_trf-3.2.1/he_ner_news_trf-3.2.1.tar.gz
\ No newline at end of file
diff --git a/scripts/wikidata_lexicon_extractor.py b/scripts/wikidata_lexicon_extractor.py
new file mode 100644
index 0000000..b4c87a8
--- /dev/null
+++ b/scripts/wikidata_lexicon_extractor.py
@@ -0,0 +1,77 @@
+"""
+A script to extract wiki datalists in Hebrew given a catagory wiki_id
+- Medication wiki_id is Q12140
+- Body_parts can be obtained by union over "anatomical structure" Q4936952, "anatomical region" (Q17781690)\
+and tagma (Q1402830)
+- hospital (Q16917)
+- city (Q515)
+- government program (Q22222786)
+
+You can explore wikidata for ideas of relevant categories https://www.wikidata.org/wiki/Wikidata:Main_Page
+
+Usage example:
+python wikidata_lexicon_extractor.py Q515 cities_lexicon.py CITIES_LEXICON
+"""
+
+from typing import List
+import requests
+import re
+import json
+import argparse
+
+WIKI_DATA_URL = 'https://query.wikidata.org/sparql'
+QUERY_TEMPLATE = """
+SELECT ?item ?itemLabel ?itemAltLabel
+WHERE 
+{
+  ?item wdt:P31 wd:<WIKI_ID>.
+  SERVICE wikibase:label { bd:serviceParam wikibase:language "he". } # Helps get the label in your language, if not, then en language
+}
+"""
+
+EN_LETTERS_RE = re.compile(r"[a-zA-Z]") # used to filter out terms with En letters
+HE_LETTERS_RE = re.compile(r"[ \-א-ת0-9]") # used to clean "nikud"
+
+
+def wikidata_query(wiki_id: str):
+    r = requests.get(WIKI_DATA_URL, params={'format': 'json', 'query': QUERY_TEMPLATE.replace("<WIKI_ID>", wiki_id)})
+    data = r.json()
+    return data["results"]['bindings']
+
+
+def process_wikidata_response(data: List) -> List[str]:
+    all_names = set()
+    for item in data:
+        label = item['item']['value']
+        alt_labels = item['itemAltLabel']['value'].split(', ') if 'itemAltLabel' in item else []
+        for name in [label] + alt_labels:
+            if not EN_LETTERS_RE.findall(name):
+                name_cleaned = ''.join(HE_LETTERS_RE.findall(name))
+                if name_cleaned:
+                    all_names.add(name_cleaned)
+    return list(all_names)
+
+
+def save_python_file(all_names: List[str], output_file: str, lexicon_name: str):
+    with open(output_file, 'w', encoding="utf-8") as fp:
+        fp.write(f"{lexicon_name} = [\n")
+        for name in all_names:
+            fp.write(f'\t"{name}",\n')
+        fp.write(']\n')
+
+
+def main(wiki_id: str, output_file: str, lexicon_name: str):
+    data = wikidata_query(wiki_id)
+    all_names = process_wikidata_response(data)
+    save_python_file(all_names, output_file, lexicon_name)
+
+
+if __name__ == "__main__":
+    parser = argparse.ArgumentParser()
+    parser.add_argument('wiki_id', help="a wikiID you want to list all instances of")
+    parser.add_argument('output_file', help="output python file")
+    parser.add_argument('lexicon_name', help="variable name in python file")
+
+    args = parser.parse_args()
+
+    main(args.wiki_id, args.output_file, args.lexicon_name)
diff --git a/server.py b/server.py
new file mode 100644
index 0000000..91f66c3
--- /dev/null
+++ b/server.py
@@ -0,0 +1,147 @@
+from typing import List, Dict, Union
+
+import uvicorn
+from fastapi import FastAPI, Response, status, Body
+from pydantic import BaseModel
+
+from hebsafeharbor import Doc
+from hsh_service import hsh_service
+
+app = FastAPI()
+
+
+class ReadyResponse(BaseModel):
+    service: str
+    status: str
+
+
+class DocsRequest(BaseModel):
+    docs: List[Dict[str, str]]
+
+
+class DocItem(BaseModel):
+    # TODO consider adding confidence score, etc.
+    text: str
+    textStartPosition: int
+    textEndPosition: int
+    textEntityType: str
+    explanation: str
+    mask: str
+    maskStartPosition: int
+    maskEndPosition: int
+    maskOperator: str
+
+
+class DocResponse(BaseModel):
+    id: str
+    text: str
+    items: List[DocItem]
+
+    class Config:
+        schema_extra = {
+            "example":
+                {
+                    "id": "doc_1",
+                    "text": "<שם_> הגיע <תאריך_> <ארגון_> עם תלונות על כאבים בחזה",
+                    "items": [
+                        {
+                            "text": "גדעון לבנה",
+                            "textStartPosition": 0,
+                            "textEndPosition": 10,
+                            "textEntityType": "PERS",
+                            "explanation": "HebSpacy",
+                            "mask": "<שם_>",
+                            "maskStartPosition": 0,
+                            "maskEndPosition": 5,
+                            "maskOperator": "replace_in_hebrew"
+                        },
+                        {
+                            "text": "ב16.1.2022",
+                            "textStartPosition": 16,
+                            "textEndPosition": 26,
+                            "textEntityType": "DATE",
+                            "explanation": "HebSpacy",
+                            "mask": "<תאריך_>",
+                            "maskStartPosition": 11,
+                            "maskEndPosition": 19,
+                            "maskOperator": "replace_in_hebrew"
+                        },
+                        {
+                            "text": "לבית החולים שערי צדק",
+                            "textStartPosition": 27,
+                            "textEndPosition": 47,
+                            "textEntityType": "ORG",
+                            "explanation": "HebSpacy",
+                            "mask": "<ארגון_>",
+                            "maskStartPosition": 20,
+                            "maskEndPosition": 28,
+                            "maskOperator": "replace_in_hebrew"
+                        }
+                    ]
+                }
+
+        }
+
+
+class DocsResponse(BaseModel):
+    docs: List[DocResponse]
+
+
+def convert_to_response(doc: Doc) -> DocResponse:
+    """
+    Converts the Doc object to Pydanic DocResponse that would be returned to the client
+    """
+    items = []
+    for mention, mask in zip(doc.consolidated_results, doc.anonymized_text.items):
+        item_response = DocItem(text=doc.text[mention.start:mention.end],
+                                textStartPosition=mention.start,
+                                textEndPosition=mention.end,
+                                textEntityType=mention.entity_type,
+                                explanation=mention.analysis_explanation.recognizer,
+                                mask=mask.text,
+                                maskStartPosition=mask.start,
+                                maskEndPosition=mask.end,
+                                maskOperator=mask.operator)
+        items.append(item_response)
+
+    doc_response = DocResponse(id=doc.id, text=doc.anonymized_text.text, items=items)
+    return doc_response
+
+
+@app.get("/")
+async def root():
+    return {"message": "Welcome to the Hebrew Safe Harbor!"}
+
+
+@app.post(path="/query", response_model=DocsResponse)
+async def query(request: DocsRequest = Body(
+    {
+        "docs": [
+            {
+                "id": "doc_1",
+                "text": "גדעון לבנה הגיע ב-16.1.2022 לבית החולים שערי צדק עם תלונות על כאבים בחזה"
+            }
+        ]
+    }), response: Response = status.HTTP_200_OK):
+    print(request)
+    docs_result, response.status_code = hsh_service.query(request.docs)
+    if response.status_code == status.HTTP_200_OK:
+        results = []
+        for doc in docs_result:
+            doc_response = convert_to_response(doc)
+            results.append(doc_response)
+        docs_response = DocsResponse(docs=results)
+        return docs_response
+    return docs_result
+
+
+@app.get(path='/ready', response_model=ReadyResponse)
+def ready(response: Response):
+    result, response.status_code = hsh_service.ready()
+    return result
+
+
+hsh_service.load_async()
+
+if __name__ == '__main__':
+    uvicorn.run(app, host="0.0.0.0", port=8080)
diff --git a/setup.cfg b/setup.cfg
new file mode 100644
index 0000000..3287428
--- /dev/null
+++ b/setup.cfg
@@ -0,0 +1,3 @@
+# Inside of setup.cfg
+[metadata]
+description-file = README.md
diff --git a/setup.py b/setup.py
new file mode 100644
index 0000000..660bea3
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,61 @@
+import os
+from os import path
+
+from setuptools import setup, find_packages
+
+this_directory = path.abspath(path.dirname(__file__))
+parent_directory = os.path.abspath(os.path.join(this_directory, os.pardir))
+
+VERSION = {}
+with open(f"{this_directory}/hebsafeharbor/version.py", "r") as version_file:
+    exec(version_file.read(), VERSION)
+
+def get_requirements_from_files(*files):
+    install_requires = {}
+    for filename in files:
+        if os.path.isfile(filename):
+            with open(filename) as f:
+                for line in f.readlines():
+                    line = line.strip()
+                    if line and not line.startswith("#"):
+                        if "#" in line:
+                            req, version = line.split("#")[0].strip().split("==")
+                        else:
+                            req, version = line, None
+
+                        # requirement versions must be specified with "==", otherwise the line above will fail
+                        if req not in install_requires:
+                            install_requires[req] = version
+    reqs = []
+    for k, v in sorted(install_requires.items()):
+        if v:
+            reqs.append(f"{k}=={v}")
+        else:
+            reqs.append(k)
+    return reqs
+
+
+install_requires_ = get_requirements_from_files('requirements.txt')
+
+for requirement in install_requires_:
+    print("adding requirement: " + requirement)
+
+with open(path.join(this_directory, "README.MD"), encoding="utf-8") as f:
+    long_description = f.read()
+
+setup(
+    name="hebsafeharbor",
+    author="hebsafeharbor",
+    version=VERSION["VERSION"],
+    author_email="hebsafeharbor@gmail.com",
+    url="https://github.com/8400TheHealthNetwork/HebSafeHarbor",
+    license="MIT License (MIT)",
+    include_package_data=True,
+    long_description=long_description,
+    long_description_content_type="text/markdown",
+    description="De-identification toolkit for clinical text in Hebrew",
+    keywords=["hebrew nlp spacy SpaCy phi pii"],
+    packages=find_packages(exclude=["demo"]),
+    install_requires=install_requires_,
+    python_requires=">=3.8.0",
+)


Test patch:
diff --git a/tests/__init__.py b/tests/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/conftest.py b/tests/conftest.py
new file mode 100644
index 0000000..30bc8cb
--- /dev/null
+++ b/tests/conftest.py
@@ -0,0 +1,7 @@
+import pytest
+import spacy
+
+
+@pytest.fixture(scope="session")
+def he_vocab():
+    return spacy.util.get_lang_class("he")().vocab
diff --git a/tests/test_spacy_recognizer_with_confidence.py b/tests/test_spacy_recognizer_with_confidence.py
new file mode 100644
index 0000000..8946698
--- /dev/null
+++ b/tests/test_spacy_recognizer_with_confidence.py
@@ -0,0 +1,62 @@
+import copy
+
+import pytest
+from presidio_analyzer.nlp_engine import NlpArtifacts
+from spacy.tokens import Span, Doc
+
+from hebsafeharbor.identifier.signals import SpacyRecognizerWithConfidence
+
+
+@pytest.fixture(autouse=True)
+def remove_extension():
+    """
+    Remove extensions as it is global
+    """
+    if Span.has_extension("confidence_score"):
+        Span.remove_extension("confidence_score")
+
+
+def doc_to_nlp_artifact(doc: Doc, language: str = "he") -> NlpArtifacts:
+        lemmas = [token.lemma_ for token in doc]
+        tokens_indices = [token.idx for token in doc]
+        entities = list(doc.ents)
+        return NlpArtifacts(
+            entities=entities,
+            tokens=doc,
+            tokens_indices=tokens_indices,
+            lemmas=lemmas,
+            nlp_engine=None,
+            language=language,
+        )
+
+
+def test_spacy_with_confidence_simple(he_vocab):
+    Span.set_extension("confidence_score", default=1.0, force=True)
+    mock_doc = Doc(he_vocab, words=["שלום", "כיתה", "אלף"])
+    mock_doc.ents = [Span(mock_doc, 0, 1, label="PERSON"), Span(mock_doc, 1, 2, label="LOC")]
+    mock_doc.ents[0]._.confidence_score = 0.666
+    mock_doc.ents[1]._.confidence_score = 0.999
+
+    nlp_artifacts = doc_to_nlp_artifact(mock_doc)
+
+    hebspacy_recognizer = SpacyRecognizerWithConfidence(supported_language="he")
+    results = hebspacy_recognizer.analyze(text=mock_doc.text,
+                                          entities=["PERSON", "LOCATION"],
+                                          nlp_artifacts=nlp_artifacts)
+
+    assert len(results) == 2
+    assert results[0].score == 0.666
+    assert results[0].entity_type == "PERSON"
+    assert results[1].score == 0.999
+    assert results[1].entity_type == "LOCATION"
+
+
+def test_spacy_without_confidence_raises_error(he_vocab):
+    mock_doc = Doc(he_vocab, words=["חיים", "כיתה", "אלף"])
+    mock_doc.ents = [Span(mock_doc, 0, 1, label="PERSON")]
+    nlp_artifacts = doc_to_nlp_artifact(mock_doc)
+
+    hebspacy_recognizer = SpacyRecognizerWithConfidence(supported_language="he")
+
+    with pytest.raises(ValueError):
+        hebspacy_recognizer.analyze(text=mock_doc.text, entities=["PERSON"], nlp_artifacts=nlp_artifacts)