EN · DE · RU · FR · ES

#830 : AddressTextParser.kt

projectforge-business/src/main/kotlin/org/projectforge/business/address/AddressTextParser.kt Classe Kotlin, projectforge-business/src/main/kotlin/org/projectforge/business/address/AddressTextParser.kt 369 lignes · 224 code · 91 commentaires · 54 vides
Objectif : Fichier source : projectforge/business/address/AddressTextParser.kt. AddressTextParser.kt fait partie de l'application open-source de gestion de projet ProjectForge.

Source (100 premières lignes)

/////////////////////////////////////////////////////////////////////////////
//
// Projet ProjectForge Community Edition
//         www.projectforge.org
//
// Copyright (C) 2001-2026 Micromata GmbH, Allemagne (www.micromata.com)
//
// ProjectForge est sous double licence.
//
// Cette édition communautaire est un logiciel libre ; vous pouvez la redistribuer et/ou
// la modifier selon les termes de la GNU General Public License telle que publiée
// par la Free Software Foundation ; version 3 de la Licence.
//
// Cette édition communautaire est distribuée dans l'espoir qu'elle sera utile,
// mais SANS AUCUNE GARANTIE ; sans même la garantie implicite de
// COMMERCIALISATION ou D'ADAPTATION À UN USAGE PARTICULIER. Voir la GNU General
// Public License pour plus de détails.
//
// Vous devriez avoir reçu une copie de la GNU General Public License avec
// ce programme ; sinon, consultez http://www.gnu.org/licenses/.
//
/////////////////////////////////////////////////////////////////////////////

package org.projectforge.business.address

import mu.KotlinLogging

private val log = KotlinLogging.logger {}

/**
 * Analyseur pour extraire les informations d'adresse à partir de texte libre (par exemple, signatures email).
 */
object AddressTextParser {


    // Suffixes d'entreprise
    private val COMPANY_SUFFIXES = listOf(
        "GmbH",
        "AG",
        "e\\.V\\.",
        "KG",
        "OHG",
        "UG",
        "SE",
        "Ltd\\.",
        "Inc\\.",
        "Corp\\.",
        "LLC",
        "PLC"
    )

    // Regex email
    private val EMAIL_REGEX = Regex(
        """[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}""",
        RegexOption.IGNORE_CASE
    )

    // Regex site web avec TLD connus pour éviter les faux positifs comme "Dipl.Phys"
    // Note : Les TLD plus longs sont listés avant les plus courts pour garantir une correspondance correcte (ex. "group" avant "gr")
    private val WEBSITE_REGEX = Regex(
        """(?:https?://)?(?:www\.)?[a-zA-Z0-9.-]+\.(?:solutions|company|academy|digital|center|online|store|group|cloud|gmbh|tech|info|shop|app|dev|pro|com|org|net|edu|gov|biz|aero|asia|coop|jobs|mobi|museum|name|post|tel|travel|xxx|de|uk|us|io|co|eu|ch|at|fr|it|es|nl|be|pl|ru|jp|cn|au|ca|nz|se|no|dk|fi|in|br|mx|za|kr|tw|hk|sg|my|th|vn|ph|id|ae|sa|il|tr|gr|cz|sk|hu|ro|bg|hr|si|lt|lv|ee|is|ie|pt|lu|mt|cy|ai)(?:/[^\s]*)?""",
        RegexOption.IGNORE_CASE
    )

    // Regex téléphone (divers formats avec séparateurs flexibles)
    // Correspond aux numéros de téléphone avec chiffres et séparateurs courants (espaces, -, /, ., parenthèses)
    // Prend en charge "Tel:" et "Tel" (avec/sans deux-points)
    private val PHONE_REGEX = Regex(
        """(?:Tel\.?:?|Telefon:?|Phone:?|Fon:?|Mobil:?|Mobile:?|Fax:?)\s*(\+?(?:\d+[\s\-./()]*)+\d)""",
        RegexOption.IGNORE_CASE
    )

    // Regex téléphone nu (numéro de téléphone sans étiquette de préfixe)
    // Correspond aux numéros de téléphone qui commencent par + ou un indicatif pays et ont au moins 8 chiffres
    // Cela évite les faux positifs avec les numéros ordinaires
    private val BARE_PHONE_REGEX = Regex(
        """^\+?(?:\d+[\s\-./()]*){8,}$"""
    )

    // Code postal + Ville (4-5 chiffres + nom de ville, optionnellement avec préfixe "D-" ou "CH-")
    // Prend en charge l'allemand (5 chiffres), le suisse (4 chiffres) et d'autres formats
    private val ZIP_CITY_REGEX = Regex(
        """(?:D-|CH-)?(\d{4,5})\s+([A-ZÄÖÜ][a-zäöüß]+(?:[\s-][A-ZÄÖÜ]?[a-zäöüß]+)*)""",
    )

    // Adresse rue (nom de rue + numéro de maison)
    private val STREET_REGEX = Regex(
        """([A-ZÄÖÜ][a-zäöüß]+(?:[\s-][A-ZÄÖÜ]?[a-zäöüß]+)*\.?(?:\s+|-)(?:\d+[a-zA-Z]?(?:\s*-\s*\d+[a-zA-Z]?)?))""",
    )

    // Nom de pays (pays courants en plusieurs langues, optionnellement avec deuxième nom après /)
    private val COUNTRY_REGEX = Regex(
        """^(Deutschland|Germany|Schweiz|Switzerland|Österreich|Austria|France|Frankreich|Italia?|Italy|UK|USA|United States|United Kingdom|Nederland|Netherlands|Belgique|Belgium|España|Spain|Portugal|Sverige|Sweden|Norge|Norway|Danmark|Denmark|Polska|Poland|Česko|Czech Republic|Slovensko|Slovakia|Magyarország|Hungary|România|Romania|Bulgarien|Bulgaria|Ellinikí Demokratía|Greece|Türkiye|Turkey|Россия|Russia)(?:\s*/\s*(?:Deutschland|Germany|Schweiz|Switzerland|Österreich|Austria|France|Frankreich|Italia?|Italy|UK|USA|United States|United Kingdom|Nederland|Netherlands|Belgique|Belgium|España|Spain|Portugal|Sverige|Sweden|Norge|Norway|Danmark|Denmark|Polska|Poland|Česko|Czech Republic|Slovensko|Slovakia|Magyarország|Hungary|România|Romania|Bulgarien|Bulgaria|Ellinikí Demokratía|Greece|Türkiye|Turkey|Россия|Russia))?$""",
        RegexOption.IGNORE_CASE
    )

    /**
     * Analyse le texte libre et extrait les informations d'adresse.
     */
    fun parseAddressText(text: String): ParsedAddressData {

Historique Git

868d6abb7 2025 -> 2026
30ec0db73 AddressImportReconciler
cab29b70b WIP : AddressTextParser : PersonNameParser introduit pour une meilleure analyse des titres, formules de politesse, etc.
48e37a4c9 AddressTextParser : PersonNameParser introduit pour une meilleure analyse des titres, formules de politesse, etc.
0b1ab35a7 AddressTextParser : PersonNameParser introduit pour une meilleure analyse des titres, formules de politesse, etc.

868d6abb7

2025 -> 2026
868d6abb75cd191a892911ac8e45058932cf9074
diff --git a/projectforge-business/src/main/kotlin/org/projectforge/business/address/AddressTextParser.kt b/projectforge-business/src/main/kotlin/org/projectforge/business/address/AddressTextParser.kt
index 6c334b22b..a4d96916a 100644
--- a/projectforge-business/src/main/kotlin/org/projectforge/business/address/AddressTextParser.kt
+++ b/projectforge-business/src/main/kotlin/org/projectforge/business/address/AddressTextParser.kt
@@ -3,7 +3,7 @@
 // Projet ProjectForge Community Edition
 //         www.projectforge.org
 //
-// Copyright (C) 2001-2025 Micromata GmbH, Allemagne (www.micromata.com)
+// Copyright (C) 2001-2026 Micromata GmbH, Allemagne (www.micromata.com)
 //
 // ProjectForge est sous double licence.
 //

30ec0db73

AddressImportReconciler
30ec0db73e57c418559d0d754bfceb90c1997db2
diff --git a/projectforge-business/src/main/kotlin/org/projectforge/business/address/AddressTextParser.kt b/projectforge-business/src/main/kotlin/org/projectforge/business/address/AddressTextParser.kt
index c2f8e1068..6c334b22b 100644
--- a/projectforge-business/src/main/kotlin/org/projectforge/business/address/AddressTextParser.kt
+++ b/projectforge-business/src/main/kotlin/org/projectforge/business/address/AddressTextParser.kt
@@ -70,6 +70,13 @@ object AddressTextParser {
         RegexOption.IGNORE_CASE
     )
 
+    // Regex téléphone nu (numéro de téléphone sans étiquette de préfixe)
+    // Correspond aux numéros de téléphone qui commencent par + ou un indicatif pays et ont au moins 8 chiffres
+    // Cela évite les faux positifs avec les numéros ordinaires
+    private val BARE_PHONE_REGEX = Regex(
+        """^\+?(?:\d+[\s\-./()]*){8,}$"""
+    )
+
     // Code postal + Ville (4-5 chiffres + nom de ville, optionnellement avec préfixe "D-" ou "CH-")
     // Prend en charge l'allemand (5 chiffres), le suisse (4 chiffres) et d'autres formats
     private val ZIP_CITY_REGEX = Regex(
@@ -135,7 +142,7 @@ object AddressTextParser {
                 }
             }
 
-            // Extraire les numéros de téléphone
+            // Extraire les numéros de téléphone avec préfixe (Tel:, Phone:, etc.)
             if (line.matches(Regex(""".*(?:Tel\.?:?|Telefon:?|Phone:?|Fon:?|Mobil:?|Mobile:?|Fax:?).*""", RegexOption.IGNORE_CASE))) {
                 val phoneMatch = PHONE_REGEX.find(line)
                 if (phoneMatch != null) {
@@ -161,6 +168,25 @@ object AddressTextParser {
                 }
             }
 
+            // Extraire les numéros de téléphone nus (sans préfixe)
+            if (!processed && BARE_PHONE_REGEX.matches(line)) {
+                val phone = line.trim()
+                phoneNumbers.add(phone)
+
+                // Normaliser le numéro de téléphone
+                val normalizedPhone = org.projectforge.framework.utils.PhoneNumberUtils.normalizePhoneNumber(phone)
+
+                // Sans étiquette, supposer qu'il s'agit d'un téléphone professionnel (sauf si nous en avons déjà un, alors mobile)
+                if (result.businessPhone == null) {
+                    result.businessPhone = normalizedPhone
+                } else if (result.mobilePhone == null) {
+                    result.mobilePhone = normalizedPhone
+                } else if (result.fax == null) {
+                    result.fax = normalizedPhone
+                }
+                processed = true
+            }
+
             // Extraire le code postal + Ville (peut être combiné avec la rue dans la même ligne)
             ZIP_CITY_REGEX.find(line)?.let {
                 if (result.zipCode == null) {

cab29b70b

WIP : AddressTextParser : PersonNameParser introduit pour une meilleure analyse des titres, formules de politesse, etc.
cab29b70bfb32b34408aae8db933f60ec04bc405
diff --git a/projectforge-business/src/main/kotlin/org/projectforge/business/address/AddressTextParser.kt b/projectforge-business/src/main/kotlin/org/projectforge/business/address/AddressTextParser.kt
index 9b55d3d6a..c2f8e1068 100644
--- a/projectforge-business/src/main/kotlin/org/projectforge/business/address/AddressTextParser.kt
+++ b/projectforge-business/src/main/kotlin/org/projectforge/business/address/AddressTextParser.kt
@@ -55,9 +55,10 @@ object AddressTextParser {
         RegexOption.IGNORE_CASE
     )
 
-    // Regex site web
+    // Regex site web avec TLD connus pour éviter les faux positifs comme "Dipl.Phys"
+    // Note : Les TLD plus longs sont listés avant les plus courts pour garantir une correspondance correcte (ex. "group" avant "gr")
     private val WEBSITE_REGEX = Regex(
-        """(?:https?://)?(?:www\.)?[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}(?:/[^\s]*)?""",
+        """(?:https?://)?(?:www\.)?[a-zA-Z0-9.-]+\.(?:solutions|company|academy|digital|center|online|store|group|cloud|gmbh|tech|info|shop|app|dev|pro|com|org|net|edu|gov|biz|aero|asia|coop|jobs|mobi|museum|name|post|tel|travel|xxx|de|uk|us|io|co|eu|ch|at|fr|it|es|nl|be|pl|ru|jp|cn|au|ca|nz|se|no|dk|fi|in|br|mx|za|kr|tw|hk|sg|my|th|vn|ph|id|ae|sa|il|tr|gr|cz|sk|hu|ro|bg|hr|si|lt|lv|ee|is|ie|pt|lu|mt|cy|ai)(?:/[^\s]*)?""",
         RegexOption.IGNORE_CASE
     )
 
@@ -118,10 +119,18 @@ object AddressTextParser {
 
             // Extraire le site web (mais pas l'email)
             if (!line.contains("@")) {
-                WEBSITE_REGEX.find(line)?.let {
-                    if (result.website == null && !it.value.contains("@")) {
-                        result.website = it.value
-                        processed = true
+                WEBSITE_REGEX.find(line)?.let { match ->
+                    if (result.website == null && !match.value.contains("@")) {
+                        // Vérification supplémentaire : Si 2+ mots suivent la correspondance, il s'agit probablement d'un nom avec titre (ex. "Dipl.Phys Max Mustermann")
+                        val remainingText = line.substring(match.range.last + 1).trim()
+                        val wordsAfter = remainingText.split(Regex("""\s+""")).filter { it.isNotBlank() }
+
+                        if (wordsAfter.size < 2) {
+                            // Probablement un vrai site web
+                            result.website = match.value
+                            processed = true
+                        }
+                        // Si 2+ mots suivent, ignorer cette correspondance (probablement un titre + nom)
                     }
                 }
             }
@@ -285,6 +294,11 @@ object AddressTextParser {
             result.title = parsedName.titles.joinToString(" ")
         }
 
+        // Définir la formule de politesse (correspond à AddressDO.form)
+        if (parsedName.formOfAddress != null) {
+            result.form = parsedName.formOfAddress
+        }
+
         // Définir le prénom et le nom de famille
         if (parsedName.firstName.isNotEmpty()) {
             result.firstName = parsedName.firstName

48e37a4c9

AddressTextParser : PersonNameParser introduit pour une meilleure analyse des titres, formules de politesse, etc.
48e37a4c92a17e99e7d8d7440d4d35a1fcabf3ce
diff --git a/projectforge-business/src/main/kotlin/org/projectforge/business/address/AddressTextParser.kt b/projectforge-business/src/main/kotlin/org/projectforge/business/address/AddressTextParser.kt
index 307a08040..9b55d3d6a 100644
--- a/projectforge-business/src/main/kotlin/org/projectforge/business/address/AddressTextParser.kt
+++ b/projectforge-business/src/main/kotlin/org/projectforge/business/address/AddressTextParser.kt
@@ -277,8 +277,8 @@ object AddressTextParser {
             remainingLine = remainingLine.substring(iAMatch.value.length).trim()
         }
 
-        // Utiliser NameParser pour extraire les titres, formules de politesse, prénom et nom
-        val parsedName = NameParser.parse(remainingLine)
+        // Utiliser PersonNameParser pour extraire les titres, formules de politesse, prénom et nom
+        val parsedName = PersonNameParser.parse(remainingLine)
 
         // Définir le titre (joindre tous les titres avec un espace)
         if (parsedName.titles.isNotEmpty()) {

0b1ab35a7

AddressTextParser : PersonNameParser introduit pour une meilleure analyse des titres, formules de politesse, etc.
0b1ab35a73e093d9af27668996430ed46819a4ba
diff --git a/projectforge-business/src/main/kotlin/org/projectforge/business/address/AddressTextParser.kt b/projectforge-business/src/main/kotlin/org/projectforge/business/address/AddressTextParser.kt
index fa1e7e4ab..307a08040 100644
--- a/projectforge-business/src/main/kotlin/org/projectforge/business/address/AddressTextParser.kt
+++ b/projectforge-business/src/main/kotlin/org/projectforge/business/address/AddressTextParser.kt
@@ -32,21 +32,6 @@ private val log = KotlinLogging.logger {}
  */
 object AddressTextParser {
 
-    // Titres académiques et professionnels courants en allemand et anglais
-    private val TITLE_PATTERNS = listOf(
-        "Dr\\.",
-        "Prof\\.",
-        "Dipl\\.-Kfm\\.",
-        "Dipl\\.-Ing\\.",
-        "Dipl\\.-Inf\\.",
-        "Dipl\\.",
-        "B\\.Sc\\.",
-        "M\\.Sc\\.",
-        "B\\.A\\.",
-        "M\\.A\\.",
-        "Ph\\.D\\.",
-        "MBA"
-    )
 
     // Suffixes d'entreprise
     private val COMPANY_SUFFIXES = listOf(
@@ -292,27 +277,20 @@ object AddressTextParser {
             remainingLine = remainingLine.substring(iAMatch.value.length).trim()
         }
 
-        // Extraire le titre si présent
-        for (titlePattern in TITLE_PATTERNS) {
-            val titleRegex = Regex("""^($titlePattern)\s*""")
-            val match = titleRegex.find(remainingLine)
-            if (match != null) {
-                result.title = match.groupValues[1]
-                remainingLine = remainingLine.substring(match.value.length).trim()
-                break
-            }
+        // Utiliser NameParser pour extraire les titres, formules de politesse, prénom et nom
+        val parsedName = NameParser.parse(remainingLine)
+
+        // Définir le titre (joindre tous les titres avec un espace)
+        if (parsedName.titles.isNotEmpty()) {
+            result.title = parsedName.titles.joinToString(" ")
         }
 
-        // Diviser le reste en prénom et nom de famille
-        val nameParts = remainingLine.split(Regex("""\s+"""))
-        when {
-            nameParts.size >= 2 -> {
-                result.firstName = nameParts[0]
-                result.name = nameParts.drop(1).joinToString(" ")
-            }
-            nameParts.size == 1 -> {
-                result.name = nameParts[0]
-            }
+        // Définir le prénom et le nom de famille
+        if (parsedName.firstName.isNotEmpty()) {